#!/usr/bin/env bash # docker-finance | modern accounting for the power-user # # Copyright (C) 2021-2026 Aaron Fiore (Founder, Evergreen Crypto LLC) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # "Libraries" # [ -z "$DOCKER_FINANCE_CONTAINER_REPO" ] && exit 1 source "${DOCKER_FINANCE_CONTAINER_REPO}/src/finance/lib/internal/lib_utils.bash" || exit 1 # # Facade # function lib_reports::reports() { lib_reports::__parse_args "$@" time lib_reports::__reports "$@" lib_utils::catch $? } # # Implementation # function lib_reports::__parse_args() { [ -z "$global_usage" ] && lib_utils::die_fatal [ -z "$global_arg_delim_2" ] && lib_utils::die_fatal [ -z "$global_arg_delim_3" ] && lib_utils::die_fatal local -r _usage=" \e[32mDescription:\e[0m Generate financial reports (income statement, balance sheet, etc.) \e[32mUsage:\e[0m $ $global_usage [all${global_arg_delim_2}] [type${global_arg_delim_2}] \e[32mArguments:\e[0m All options: all${global_arg_delim_2} Report type: type${global_arg_delim_2} acc = accounts bs = balance sheet bse = balance sheet w/ equity cf = cash flow is = income statement Report interval (period): interval${global_arg_delim_2} Report year: year${global_arg_delim_2} \e[32mExamples:\e[0m \e[37;2m# Generate all type and all intervals for current year\e[0m $ $global_usage all${global_arg_delim_2}all \e[37;2m# Generate all types for annual report for year 2022\e[0m $ $global_usage all${global_arg_delim_2}type interval${global_arg_delim_2}annual year${global_arg_delim_2}2022 \e[37;2m# Generate income statements and balance sheets, quarterly and monthly for current year\e[0m $ $global_usage type${global_arg_delim_2}is${global_arg_delim_3}bs interval${global_arg_delim_2}quarterly${global_arg_delim_3}monthly " # # Ensure supported arguments # [ $# -eq 0 ] && lib_utils::die_usage "$_usage" for _arg in "$@"; do [[ ! "$_arg" =~ ^all${global_arg_delim_2} ]] \ && [[ ! "$_arg" =~ ^type[s]?${global_arg_delim_2} ]] \ && [[ ! "$_arg" =~ ^interval[s]?${global_arg_delim_2} ]] \ && [[ ! "$_arg" =~ ^year[s]?${global_arg_delim_2} ]] \ && lib_utils::die_usage "$_usage" done # # Parse arguments before testing # # Parse key for value for _arg in "$@"; do local _key="${_arg%${global_arg_delim_2}*}" local _len="$((${#_key} + 1))" if [[ "$_key" =~ ^all$ ]]; then local _arg_all="${_arg:${_len}}" [ -z "$_arg_all" ] && lib_utils::die_usage "$_usage" fi if [[ "$_key" =~ ^type[s]?$ ]]; then local _arg_type="${_arg:${_len}}" [ -z "$_arg_type" ] && lib_utils::die_usage "$_usage" fi if [[ "$_key" =~ ^interval[s]?$ ]]; then local _arg_interval="${_arg:${_len}}" [ -z "$_arg_interval" ] && lib_utils::die_usage "$_usage" fi if [[ "$_key" =~ ^year[s]?$ ]]; then local _arg_year="${_arg:${_len}}" [ -z "$_arg_year" ] && lib_utils::die_usage "$_usage" fi done # # Test for valid ordering/functionality of argument values # # Arg: all if [ ! -z "$_arg_all" ]; then # Can't use with non-years activated if [[ ! -z "$_arg_type" && ! -z "$_arg_interval" ]]; then lib_utils::die_usage "$_usage" fi fi # Arg: type if [ ! -z "$_arg_type" ]; then [[ ! -z "$_arg_all" && ! -z "$_arg_interval" ]] \ || [[ -z "$_arg_all" && -z "$_arg_interval" ]] \ && lib_utils::die_usage "$_usage" fi # Arg: interval if [ ! -z "$_arg_interval" ]; then [[ ! -z "$_arg_all" && ! -z "$_arg_type" ]] \ || [[ -z "$_arg_all" && -z "$_arg_type" ]] \ && lib_utils::die_usage "$_usage" fi # Arg: year if [ ! -z "$_arg_year" ]; then [[ -z "$_arg_all" && -z "$_arg_type" && -z "$_arg_interval" ]] \ && lib_utils::die_usage "$_usage" fi # # Test argument values, set globals # IFS="$global_arg_delim_3" # Arg: all if [ ! -z "$_arg_all" ]; then # If all= {type,interval} or {interval,type} then set to all=all [[ "${#_arg_all[@]}" -eq 1 && "${_arg_all[*]}" =~ type[s]? && "${_arg_all[*]}" =~ interval[s]? ]] \ && _arg_all="all" # Read args from all read -ra _read <<<"$_arg_all" for _arg in "${_read[@]}"; do # Support values [[ ! "$_arg" =~ ^all$|^type[s]?$|^interval[s]?$ ]] \ && lib_utils::die_usage "$_usage" # If all=all then no need for all={type,interval} and type= or interval= [[ "$_arg" == "all" && (! -z "$_arg_type" || ! -z "$_arg_interval") ]] \ || [[ "$_arg" == "all" && "${#_read[@]}" -gt 1 ]] \ && lib_utils::die_usage "$_usage" # If all=type then no need for type= and if all=interval then no need for interval= [[ "$_arg" =~ ^type[s]?$ && ! -z "$_arg_type" ]] \ || [[ "$_arg" =~ ^interval[s]?$ && ! -z "$_arg_interval" ]] \ && lib_utils::die_usage "$_usage" # If all=type then need interval= or if all=interval then need type= if [[ "$_arg" != "all" ]]; then [[ "${#_read[@]}" -lt 2 && "$_arg" =~ ^type[s]?$ && -z "$_arg_interval" ]] \ || [[ "${#_read[@]}" -lt 2 && "$_arg" =~ ^interval[s]?$ && -z "$_arg_type" ]] \ && lib_utils::die_usage "$_usage" fi done declare -gr global_arg_all=("${_read[@]}") fi # Arg: type # TODO: acc should not require (nor produce based upon) interval if [ ! -z "$_arg_type" ]; then read -ra _read <<<"$_arg_type" for _arg in "${_read[@]}"; do [[ ! "$_arg" =~ ^acc$|^bs$|^bse$|^cf$|^is$ ]] && lib_utils::die_usage "$_usage" done declare -gr global_arg_type=("${_read[@]}") elif [[ "${global_arg_all[*]}" =~ (all|type) ]]; then declare -gr global_arg_type=("acc" "bs" "bse" "cf" "is") fi # Arg: interval if [ ! -z "$_arg_interval" ]; then read -ra _read <<<"$_arg_interval" for _arg in "${_read[@]}"; do [[ ! "$_arg" =~ ^daily$|^weekly$|^monthly$|^quarterly$|^yearly$|^annual$ ]] \ && lib_utils::die_usage "$_usage" done declare -gr global_arg_interval=("${_read[@]}") elif [[ "${global_arg_all[*]}" =~ (all|interval) ]]; then declare -gr global_arg_interval=("daily" "weekly" "monthly" "quarterly" "annual") fi # Arg: year # TODO: implement range # TODO: implement all years if [ ! -z "$_arg_year" ]; then # TODO: 20th century support if [[ ! "$_arg_year" =~ ^20[0-9][0-9]$ ]]; then lib_utils::die_usage "$_usage" fi declare -gr global_arg_year="$_arg_year" else global_arg_year="$(date +%Y)" declare -gr global_arg_year fi } function lib_reports::__reports() { [ -z "$global_parent_profile" ] && lib_utils::die_fatal [ -z "$global_child_profile" ] && lib_utils::die_fatal [ -z "$global_child_profile_journal" ] && lib_utils::die_fatal local -r _year="$global_arg_year" local -r _out_dir="$(dirname $global_child_profile_journal)/reports/${_year}" [ ! -d "$_out_dir" ] && mkdir -p "$_out_dir" local -r _indv_out_dir="${_out_dir}/individual" [ ! -d "$_indv_out_dir" ] && mkdir -p "$_indv_out_dir" lib_utils::print_custom "\n" lib_utils::print_info "Generating financial reports in year '${_year}' for '${global_parent_profile}/${global_child_profile}' ..." lib_utils::print_custom "\n" lib_utils::print_normal " ─ Reports" lib_utils::print_custom " \e[32m│\e[0m\n" local _username _username="$(basename "$(dirname $global_child_profile_journal)" | tr '[:lower:]' '[:upper:]')" for _type in "${global_arg_type[@]}"; do case "$_type" in acc) local _echo="accounts" local _desc="ACCOUNTS" local _base_file="${_year}_${_username}_accounts" local _opts=("not:equity:balances\$") ;; bs) local _echo="balance sheet" local _desc="BALANCE SHEET" local _base_file="${_year}_${_username}_balance-sheet" local _opts=("") ;; bse) local _echo="balance sheet w/ equity" local _desc="BALANCE SHEET w/ EQUITY" local _base_file="${_year}_${_username}_balance-sheet-equity" # TODO: finer rule for equity-specific deposit/withdrawal local _opts=("not:equity:balances\$") #"not:deposit" "not:withdrawal") ;; cf) local _echo="cashflow" local _desc="CASHFLOW" local _base_file="${_year}_${_username}_cashflow" local _opts=("") ;; is) local _echo="income statement" local _desc="INCOME STATEMENT" local _base_file="${_year}_${_username}_income-statement" local _opts=("") ;; *) lib_utils:die_fatal "Type not implemented" ;; esac # Interval for _interval in "${global_arg_interval[@]}"; do case "$_interval" in yearly | annual) local _new_base_file="${_base_file}_01_yearly" local _new_opts=("${_opts[@]}") ;; quarterly) local _new_base_file="${_base_file}_02_${_interval}" local _new_opts=("${_opts[@]}" "--${_interval}") ;; monthly) local _new_base_file="${_base_file}_03_${_interval}" local _new_opts=("${_opts[@]}" "--${_interval}") ;; weekly) local _new_base_file="${_base_file}_04_${_interval}" local _new_opts=("${_opts[@]}" "--${_interval}") ;; daily) local _new_base_file="${_base_file}_05_${_interval}" local _new_opts=("${_opts[@]}" "--${_interval}") ;; *) lib_utils:die_fatal "Interval not implemented" ;; esac # Execute local _new_echo="$_echo (${_interval})" local _new_desc="$_desc (${_interval})" lib_utils::print_custom " \e[32m├─\e[34m\e[1;3m ${_new_echo}\e[0m\n" (lib_reports::__reports_write \ "$_type" \ "$_new_desc" \ "$_new_base_file" \ "${_new_opts[@]}") & done wait lib_utils::print_custom " \e[32m│\e[0m\n" done # TODO: more once upstream adds more features # Index of all reports lib_utils::print_normal " ─ Generating index of all reports ..." local _index="index" lib_reports::__reports_write \ "$_index" \ "$_index" \ "$_index" \ "" [ -z "$DOCKER_FINANCE_CLIENT_FLOW" ] && lib_utils::die_fatal local -r _client_out_dir="${DOCKER_FINANCE_CLIENT_FLOW}/profiles/${global_parent_profile}/${global_child_profile}/reports/${_year}" lib_utils::print_custom "\n Reports are located:\n\n" lib_utils::print_custom "\tclient (host): ${_client_out_dir}/\n\n" lib_utils::print_custom "\tcontainer: ${_out_dir}/\n" lib_utils::print_custom "\n On your client (host), open your browser to:\n\n" lib_utils::print_custom "\tfile://${_client_out_dir}/${_index}.html\n\n" lib_utils::print_info "Done!" } function lib_reports::__reports_write() { local _type="$1" local _description="$2" local _base_file="$3" local _opts=("${@:4}") # Generate HTML local _ext="html" local _file="${_base_file}.${_ext}" # HTML tag for search/replacement local _html_tag="DOCKER_FINANCE_HTML_TAG" # Generate individual files # NOTE: all valued periods are appended if [ "$_type" != "index" ]; then # Individual report local _individual="${_indv_out_dir}/${_file}" ## Base hledger command local _period=("--period" "$_year") if [ "$_year" == "$(date +%Y)" ]; then local _period=("-b" "${_year}/01/01" "-e" "$(date +%Y/%m/%d)") fi # TODO: instead of `not:desc` for opening/closing, add opening/closing tags and use `not:tag` local _base_hledger=("${global_hledger_cmd[@]}" "${_period[@]}" "$_type" "${_opts[@]}" "not:desc:balances\$" "not:archive") ## Divided sections, and the linkable section echo -e "${_year} $_username | ${_description}" >"$_individual" echo -e "

" >>"$_individual" echo -e "

${_year} $_username | ${_description}

" >>"$_individual" echo -e "
" >>"$_individual" ## Brief view echo "

BRIEF VIEW

" >>"$_individual" local _hledger=("${_base_hledger[@]}") if [ "$_type" == "acc" ]; then # Text -> HTML _hledger+=("--depth" "2") "${_hledger[@]}" | sed 's:$:
:g' >>"$_individual" lib_utils::catch $? echo -e "

" >>"$_individual" else # HTML _hledger+=("--depth" "2" "-O" "html") "${_hledger[@]}" >>"$_individual" lib_utils::catch $? echo -e "

" >>"$_individual" _hledger+=("--value=end") "${_hledger[@]}" >>"$_individual" lib_utils::catch $? fi echo -e "
" >>"$_individual" ## Full view echo "

FULL VIEW

" >>"$_individual" local _hledger=("${_base_hledger[@]}") # Reset to base if [ "$_type" == "acc" ]; then # Text -> HTML "${_hledger[@]}" | sed 's:$:
:g' >>"$_individual" lib_utils::catch $? echo -e "

" >>"$_individual" else # HTML _hledger+=("-O" "html") "${_hledger[@]}" >>"$_individual" lib_utils::catch $? echo -e "

" >>"$_individual" _hledger+=("--value=end") "${_hledger[@]}" >>"$_individual" lib_utils::catch $? fi ## Back to top echo -e "" >>"$_individual" echo -e "


" >>"$_individual" return fi # Combines HTML lines from each individual report (that has _html_tag) into links within an single index local _index="${_out_dir}/${_file}" echo "" >"$_index" ## Grab individuals ls "${_indv_out_dir}"/*.${_ext} \ | sort -r \ | while read _line1; do basename -s ."${_ext}" "$(echo "$_line1")" \ | while read _line2; do # Pull out the description to use as link sed -i "1i$(echo "

$(grep $_html_tag $_line1 | cut -d'>' -f3 | cut -d'<' -f1 | cut -d'|' -f2)

")" "$_index" done done # Additional stylings sed -i "1i \ \ \ $_year $_username REPORTS \ \ \ \
\
\

$_year $_username REPORTS

\
\
" "$_index" echo "" >>"$_index" } # vim: sw=2 sts=2 si ai et