How to produce the reports an accounting UI needs, using beancount_ex. This mirrors the reporting sections of Running Beancount and Generating Reports with the Elixir API.

Engine configuration

Reports run through the configured engine:

# Default: shells out to bean-query (requires pip install beancount beanquery)
config :beancount_ex, engine: Beancount.Engine.CLI

# Native: no bean-query required for canned reports
config :beancount_ex, engine: Beancount.Engine.Elixir

All examples below accept either a directive list or raw .bean text.

Canned reports

ledger = [...]  # or File.read!("ledger.bean")

{:ok, balances} = Beancount.balances(ledger)
{:ok, sheet} = Beancount.balance_sheet(ledger)
{:ok, income} = Beancount.income_statement(ledger)
{:ok, holdings} = Beancount.holdings(ledger)
{:ok, journal} = Beancount.journal(ledger, "Assets:Bank")
HelperPurpose
balances/1Sum of positions per account
balance_sheet/1Assets, Liabilities, Equity
income_statement/1Income and Expenses
holdings/1Units and cost for asset accounts
journal/2Transaction history for one account

Result shape

%Beancount.Query.Result{
  status: :ok,
  columns: ["account", "balance"],
  rows: [["Assets:Bank", "5000 USD"], ...]
}

Convert for JSON APIs:

Beancount.Query.Result.to_maps(result)
# => [%{"account" => "Assets:Bank", "balance" => "5000 USD"}, ...]

Custom BQL queries

bql = """
SELECT date, flag, payee, narration, position, balance
WHERE account = "Assets:Bank"
ORDER BY date
"""

{:ok, result} = Beancount.query(ledger, bql)

Use query_text/2 or query_file/2 when the ledger is already on disk.

Explorer DataFrames (optional)

Add {:explorer, "~> 0.11"} to your app's mix.exs (Explorer is not a dependency of beancount_ex itself — it still pins decimal ~> 2.1, while this library requires decimal ~> 3.1 for Ecto). Until Explorer supports Decimal 3, you may need:

{:decimal, "~> 3.1", override: true}

in your app as well. Then:

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

In Livebook, a DataFrame as the last cell renders as an interactive table. Cast numeric columns with Explorer.Series.cast/2 when needed.

Wiring a UI

Typical Phoenix or LiveView flow:

  1. Load ledger directives from your database or parse uploaded .bean text.
  2. Beancount.check/1 before save; show validation errors inline.
  3. On dashboard mount, call balance_sheet/1 and income_statement/1.
  4. Pass Query.Result.to_maps/1 to your template or charting library.

Example LiveView assign:

{:ok, result} = Beancount.balances(socket.assigns.ledger)
assign(socket, :accounts, Beancount.Query.Result.to_maps(result))

Validation without reports

When you only need pass/fail:

case Beancount.check(ledger) do
  {:ok, _} -> :valid
  {:error, %{normalized: %{errors: errors}}} -> {:invalid, errors}
end

Next