Last active
December 3, 2025 21:57
-
-
Save gnidan/ae7e0774342d3104dd753b5b405dcc6e to your computer and use it in GitHub Desktop.
`hledger balancesheetequity` with revenues+expenses retained
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # hledger-bsrx - balance sheet with retained earnings | |
| # Place in PATH as 'hledger-bsrx' to use as 'hledger bsrx' | |
| set -e | |
| usage() { | |
| cat <<EOF | |
| hledger-bsrx - balance sheet with automatic retained earnings | |
| Usage: hledger bsrx [OPTIONS] [QUERY...] | |
| Generates a balance sheet that automatically closes revenue and expense | |
| accounts to a retained earnings account. | |
| Custom flags: | |
| --retain-acct=ACCT set retained earnings account | |
| (default: equity:retained earnings) | |
| --commodity-format=FMT set commodity format for internal calculations | |
| (default: \$1,000.000000000) | |
| Standard hledger flags are supported: | |
| Query flags (-b, -e, -p, account patterns) filter both the closing | |
| entries and the final report. | |
| Report flags (--layout, -O, -t, -l, -B, -V, etc.) apply only to the | |
| final balance sheet output. | |
| Examples: | |
| hledger bsrx | |
| hledger bsrx --retain-acct='equity:earnings' ^mycompany | |
| hledger bsrx -e 2024-12-31 --tree --layout wide | |
| EOF | |
| exit 0 | |
| } | |
| log() { | |
| echo "$@" >&2 | |
| } | |
| # Defaults | |
| COMMODITY_FORMAT='$1,000.000000000' | |
| RETAINED_EARNINGS_ACCOUNT='equity:retained earnings' | |
| # Argument arrays | |
| query_args=() # dates, account patterns -> both close_entries and bse | |
| report_args=() # display/valuation flags -> only bse | |
| # Parse arguments | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -h|--help) | |
| usage | |
| ;; | |
| # Custom flags | |
| --retain-acct=*) | |
| RETAINED_EARNINGS_ACCOUNT="${1#*=}" | |
| shift | |
| ;; | |
| --retain-acct) | |
| RETAINED_EARNINGS_ACCOUNT="$2" | |
| shift 2 | |
| ;; | |
| --commodity-format=*) | |
| COMMODITY_FORMAT="${1#*=}" | |
| shift | |
| ;; | |
| --commodity-format) | |
| COMMODITY_FORMAT="$2" | |
| shift 2 | |
| ;; | |
| # Query/filter flags -> both | |
| -b|--begin|-e|--end|-p|--period|-f|--file) | |
| query_args+=("$1" "$2") | |
| shift 2 | |
| ;; | |
| -b=*|--begin=*|-e=*|--end=*|-p=*|--period=*|-f=*|--file=*) | |
| query_args+=("$1") | |
| shift | |
| ;; | |
| # Report display flags -> only bse | |
| --layout|--drop|--format|-O|--output-format|-o|--output-file) | |
| if [[ "$1" == *=* ]]; then | |
| report_args+=("$1") | |
| shift | |
| else | |
| report_args+=("$1" "$2") | |
| shift 2 | |
| fi | |
| ;; | |
| --layout=*|--drop=*|--format=*|-O=*|--output-format=*|-o=*|--output-file=*) | |
| report_args+=("$1") | |
| shift | |
| ;; | |
| -t|--tree|-l|--flat|-N|--no-total|-S|--sort-amount|-A|--average|-T|--row-total) | |
| report_args+=("$1") | |
| shift | |
| ;; | |
| --no-elide|--declared|-%|--percent|--summary-only|--transpose|--invert) | |
| report_args+=("$1") | |
| shift | |
| ;; | |
| # Valuation flags -> only bse | |
| -B|--cost|-V|--market|--value|--value=*) | |
| report_args+=("$1") | |
| shift | |
| ;; | |
| -X|--exchange) | |
| report_args+=("$1" "$2") | |
| shift 2 | |
| ;; | |
| -X=*|--exchange=*) | |
| report_args+=("$1") | |
| shift | |
| ;; | |
| # Accumulation modes -> only bse (close_entries always uses implicit behavior) | |
| -H|--historical|--cumulative|--change) | |
| report_args+=("$1") | |
| shift | |
| ;; | |
| # Calculation modes -> only bse | |
| --sum|--valuechange|--gain|--count) | |
| report_args+=("$1") | |
| shift | |
| ;; | |
| # Everything else is a query pattern -> both | |
| *) | |
| query_args+=("$1") | |
| shift | |
| ;; | |
| esac | |
| done | |
| close_entries() { | |
| local balances | |
| log "Fetching balances..." | |
| balances=$(hledger bal "${query_args[@]}" type:RX -O tsv -c "$COMMODITY_FORMAT" --layout bare --no-total -N 2>/dev/null | tail -n +2) | |
| [[ -z "$balances" ]] && return | |
| local comms | |
| comms=$(echo "$balances" | cut -f2 | sort -u) | |
| echo "$(date +%Y-%m-%d) retain earnings" | |
| local tmpdir | |
| tmpdir=$(mktemp -d) | |
| trap "rm -rf '$tmpdir'" EXIT | |
| local pids=() | |
| local i=0 | |
| for comm in $comms; do | |
| log "Spawning job for commodity: $comm" | |
| ( | |
| local cur_filter | |
| if [[ "$comm" == '$' ]]; then | |
| cur_filter='cur:\$' | |
| else | |
| cur_filter="cur:$comm" | |
| fi | |
| local cost_data | |
| cost_data=$(hledger bal "${query_args[@]}" type:RX "$cur_filter" -c "$COMMODITY_FORMAT" -B -O tsv --layout bare --no-total -N 2>/dev/null | tail -n +2) | |
| local base_currency | |
| base_currency=$(echo "$cost_data" | head -n 1 | cut -f2) | |
| paste \ | |
| <(echo "$balances" | awk -F'\t' -v c="$comm" '$2 == c') \ | |
| <(echo "$cost_data" | cut -f3) \ | |
| | while IFS=$'\t' read -r account _ amount cost; do | |
| [[ -z "$account" || -z "$amount" ]] && continue | |
| abs_cost="${cost#-}" | |
| if [[ "$amount" == -* ]]; then | |
| neg_amount="${amount#-}" | |
| else | |
| neg_amount="-$amount" | |
| fi | |
| if [[ "$comm" == "$base_currency" ]]; then | |
| printf " %-60s %s%s\n" "$account" "$base_currency" "$neg_amount" | |
| printf " %-60s %s%s\n" "$RETAINED_EARNINGS_ACCOUNT" "$base_currency" "$amount" | |
| else | |
| printf " %-60s %s %s @@ %s%s\n" "$account" "$neg_amount" "$comm" "$base_currency" "$abs_cost" | |
| printf " %-60s %s %s @@ %s%s\n" "$RETAINED_EARNINGS_ACCOUNT" "$amount" "$comm" "$base_currency" "$abs_cost" | |
| fi | |
| done | |
| ) > "$tmpdir/$i" & | |
| pids+=($!) | |
| ((i++)) | |
| done | |
| wait "${pids[@]}" | |
| cat "$tmpdir"/* | |
| } | |
| # Main execution | |
| hledger -f "${LEDGER_FILE:-$HOME/.hledger.journal}" \ | |
| -f <(close_entries) \ | |
| bse "${query_args[@]}" "${report_args[@]}" | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment