Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/trevorld/r-ledger

Imports data from plain text accounting files
https://github.com/trevorld/r-ledger

beancount hledger ledger plaintext-accounting

Last synced: 2 months ago
JSON representation

Imports data from plain text accounting files

Awesome Lists containing this project

README

        

ledger
======

.. {r setup, echo = FALSE}
knitr::opts_chunk$set(fig.path = "man/figures/README-")
options(width=80)
.. ..

.. |CRAN-status| image:: https://www.r-pkg.org/badges/version/ledger
:target: https://cran.r-project.org/package=ledger
:alt: CRAN Status Badge
.. |R-CMD-check| image:: https://github.com/trevorld/r-ledger/workflows/R-CMD-check/badge.svg
:target: https://github.com/trevorld/r-ledger/actions
:alt: R-CMD-check
.. |codecov| image:: https://codecov.io/github/trevorld/r-ledger/branch/master/graph/badge.svg
:target: https://app.codecov.io/github/trevorld/r-ledger?branch=master
:alt: Coverage Status
.. |downloads| image:: https://cranlogs.r-pkg.org/badges/ledger
:target: https://cran.r-project.org/package=ledger
:alt: RStudio CRAN mirror downloads

|CRAN-status| |R-CMD-check| |codecov| |downloads|

``ledger`` is an R package to import data from `plain text accounting `_ software like `Ledger `_, `HLedger `_, and `Beancount `_ into an R data frame for convenient analysis, plotting, and export.

Right now it supports reading in the register from ``ledger``, ``hledger``, and ``beancount`` files.

.. contents::

Installation
------------

To install the last version released to CRAN use the following command in R:

.. code:: r

install.packages("ledger")

To install the development version of the ``ledger`` package (and its R package dependencies) use the ``install_github`` function from the ``remotes`` package in R:

.. code:: r

install.packages("remotes")
remotes::install_github("trevorld/r-ledger")

This package also has some system dependencies that need to be installed depending on which plaintext accounting files you wish to read to be able to read in:

ledger
`ledger `_ (>= 3.1)

hledger
`hledger `_ (>= 1.4)

beancount
`beancount `_ (>= 2.0)

To install hledger run the following in your shell:

.. code:: bash

stack update && stack install --resolver=lts-14.3 hledger-lib-1.15.2 hledger-1.15.2 hledger-web-1.15 hledger-ui-1.15 --verbosity=error

To install beancount run the following in your shell:

.. code:: bash

pip3 install beancount

`Several pre-compiled Ledger binaries are available `_ (often found in several open source repos).

To run the unit tests you'll also need the suggested R package ``testthat``.

Examples
--------

API
+++

The main function of this package is ``register`` which reads in the register of a plaintext accounting file. This package also registers S3 methods so one can use ``rio::import`` to read in a register, a ``net_worth`` convenience function, and a ``prune_coa`` convenience function.

``register()``
~~~~~~~~~~~~~~

Here are some examples of very basic files stored within the package:

.. {r register}
library("ledger")
ledger_file <- system.file("extdata", "example.ledger", package = "ledger")
register(ledger_file)
hledger_file <- system.file("extdata", "example.hledger", package = "ledger")
register(hledger_file)
beancount_file <- system.file("extdata", "example.beancount", package = "ledger")
register(beancount_file)
.. ..

Here is an example reading in a beancount file generated by ``bean-example``:

.. {r register2}
bean_example_file <- tempfile(fileext = ".beancount")
system(paste("bean-example -o", bean_example_file), ignore.stderr=TRUE)
df <- register(bean_example_file)
print(df)
suppressPackageStartupMessages(library("dplyr"))
dplyr::filter(df, grepl("Expenses", account), grepl("trip", tags)) %>%
group_by(trip = tags, account) %>%
summarize(trip_total = sum(amount), .groups = "drop")
.. ..

Using ``rio::import()`` and ``rio::convert()``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If one has loaded in the ``ledger`` package one can also use ``rio::import`` to read in the register:

.. {r rio}
df2 <- rio::import(bean_example_file)
all.equal(df, tibble::as_tibble(df2))
.. ..

The main advantage of this is that it allows one to use ``rio::convert`` to easily convert plaintext accounting files to several other file formats such as a csv file. Here is a shell example:

.. code:: bash

bean-example -o example.beancount
Rscript --default-packages=ledger,rio -e 'convert("example.beancount", "example.csv")'

``net_worth()``
~~~~~~~~~~~~~~~

Some examples of using the ``net_worth`` function using the example files from the ``register`` examples:

.. {r net_worth}
dates <- seq(as.Date("2016-01-01"), as.Date("2018-01-01"), by="years")
net_worth(ledger_file, dates)
net_worth(hledger_file, dates)
net_worth(beancount_file, dates)
dates <- seq(min(as.Date(df$date)), max(as.Date(df$date)), by="years")
net_worth(bean_example_file, dates)
.. ..

``prune_coa()``
~~~~~~~~~~~~~~~

Some examples using the ``prune_coa`` function to simplify the "Chart of Account" names to a given maximum depth:

.. {r prune_coa}
suppressPackageStartupMessages(library("dplyr"))
df <- register(bean_example_file) %>% dplyr::filter(!is.na(commodity))
df %>% prune_coa() %>%
group_by(account, mv_commodity) %>%
summarize(market_value = sum(market_value), .groups = "drop")
df %>% prune_coa(2) %>%
group_by(account, mv_commodity) %>%
summarize(market_value = sum(market_value), .groups = "drop")
.. ..

Basic personal accounting reports
+++++++++++++++++++++++++++++++++

Here is some examples using the functions in the package to help generate
various personal accounting reports of the
beancount example generated by ``bean-example``.

First we load the (mainly tidyverse) libraries we'll be using and adjusting terminal output:

.. {r setup_things, message=FALSE}
library("ledger")
library("dplyr")
filter <- dplyr::filter
library("ggplot2")
library("scales")
library("tidyr")
library("zoo")
filename <- tempfile(fileext = ".beancount")
system(paste("bean-example -o", filename), ignore.stderr=TRUE)
df <- register(filename) %>% mutate(yearmon = zoo::as.yearmon(date)) %>%
filter(commodity=="USD")
nw <- net_worth(filename)
.. ..

Then we'll write some convenience functions we'll use over and over again:

.. {r setup2}
print_tibble_rows <- function(df) {
print(df, n=nrow(df))
}
count_beans <- function(df, filter_str = "", ...,
amount = "amount",
commodity="commodity",
cutoff=1e-3) {
commodity <- sym(commodity)
amount_var <- sym(amount)
filter(df, grepl(filter_str, account)) %>%
group_by(account, !!commodity, ...) %>%
summarize(!!amount := sum(!!amount_var), .groups = "drop") %>%
filter(abs(!!amount_var) > cutoff & !is.na(!!amount_var)) %>%
arrange(desc(abs(!!amount_var)))
}
.. ..

Basic balance sheets
~~~~~~~~~~~~~~~~~~~~

Here is some basic balance sheets (using the market value of our assets):

.. {r balance}
print_balance_sheet <- function(df) {
assets <- count_beans(df, "^Assets",
amount="market_value", commodity="mv_commodity")
print_tibble_rows(assets)
liabilities <- count_beans(df, "^Liabilities",
amount="market_value", commodity="mv_commodity")
print_tibble_rows(liabilities)
}
print(nw)
print_balance_sheet(prune_coa(df, 2))
print_balance_sheet(df)
.. ..

Basic net worth chart
~~~~~~~~~~~~~~~~~~~~~

Here is a basic chart of one's net worth from the beginning of the plaintext accounting file to today by month:

.. {r net_worth_chart, fig.width=5, fig.height=5, fig.cap="Basic net worth chart"}
next_month <- function(date) {
zoo::as.Date(zoo::as.yearmon(date) + 1/12)
}
nw_dates <- seq(next_month(min(df$date)), next_month(Sys.Date()), by="months")
df_nw <- net_worth(filename, nw_dates) %>% filter(commodity=="USD")
ggplot(df_nw, aes(x=date, y=net_worth, colour=commodity, group=commodity)) +
geom_line() + scale_y_continuous(labels=scales::dollar)
.. ..

Basic income sheets
~~~~~~~~~~~~~~~~~~~

.. {r income}

month_cutoff <- zoo::as.yearmon(Sys.Date()) - 2/12
compute_income <- function(df) {
count_beans(df, "^Income", yearmon) %>%
mutate(income = -amount) %>%
select(-amount) %>% ungroup()
}
print_income <- function(df) {
compute_income(df) %>%
filter(yearmon >= month_cutoff) %>%
spread(yearmon, income, fill=0) %>%
print_tibble_rows()
}
compute_expenses <- function(df) {
count_beans(df, "^Expenses", yearmon) %>%
mutate(expenses = amount) %>%
select(-amount) %>% ungroup()
}
print_expenses <- function(df) {
compute_expenses(df) %>%
filter(yearmon >= month_cutoff) %>%
spread(yearmon, expenses, fill=0) %>%
print_tibble_rows()
}
compute_total <- function(df) {
full_join(compute_income(prune_coa(df)) %>% select(-account),
compute_expenses(prune_coa(df)) %>% select(-account),
by=c("yearmon", "commodity")) %>%
mutate(income = ifelse(is.na(income), 0, income),
expenses = ifelse(is.na(expenses), 0, expenses),
net = income - expenses) %>%
gather(type, amount, -yearmon, -commodity)
}
print_total <- function(df) {
compute_total(df) %>%
filter(yearmon >= month_cutoff) %>%
spread(yearmon, amount, fill=0) %>%
print_tibble_rows()
}
print_total(df)
print_income(prune_coa(df, 2))
print_expenses(prune_coa(df, 2))
print_income(df)
print_expenses(df)
.. ..

And here is a plot of income, expenses, and net income over time:

.. {r income_chart, fig.width=5, fig.height=5, fig.cap="Monthly income chart"}
ggplot(compute_total(df), aes(x=yearmon, y=amount, group=commodity, colour=commodity)) +
facet_grid(type ~ .) +
geom_line() + geom_hline(yintercept=0, linetype="dashed") +
scale_x_continuous() + scale_y_continuous(labels=scales::comma)
.. ..