186 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			186 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
#!/usr/bin/env bash
 | 
						|
# shellcheck disable=SC2016
 | 
						|
# Create invoices with hledger and pandoc.
 | 
						|
# cf hledger/examples/invoicing, https://hledger.org/invoicing.html, https://martinbetz.eu/articles/pandoc-invoices
 | 
						|
set -e
 | 
						|
 | 
						|
#PROG=$(basename "$0")
 | 
						|
function usage() {
 | 
						|
    cat <<EOF
 | 
						|
--------------------------------------------------------------------------------
 | 
						|
invoice:
 | 
						|
Make markdown or pdf invoices, optionally including last month's time
 | 
						|
and expenses, from a markdown template and similarly-named .css file.
 | 
						|
Requires hledger, pandoc, awk, GNU date, envsubst, python3, sed, tail.
 | 
						|
 | 
						|
$ invoice
 | 
						|
  Show this help.
 | 
						|
 | 
						|
$ invoice TEMPLATEFILE [TIMEACCTORAMT [EXPACCTORAMT]] [FLAGS]
 | 
						|
  Print a markdown invoice on stdout.
 | 
						|
  TIMEACCTORAMT and EXPACCTORAMT are time and expense accounts to query
 | 
						|
  with hledger, or if numeric, the hours and expense amounts directly.
 | 
						|
  With --md and/or --pdf, save it as markdown / PDF in current directory.
 | 
						|
  With --txn, print sample hledger journal entries on stdout.
 | 
						|
 | 
						|
EOF
 | 
						|
}
 | 
						|
 | 
						|
ARGS=()
 | 
						|
while [[ $# -gt 0 ]]; do
 | 
						|
    key="$1"
 | 
						|
    case $key in
 | 
						|
        -h|--help)
 | 
						|
            HELP=1
 | 
						|
            shift
 | 
						|
            ;;
 | 
						|
        --md)
 | 
						|
            MD=1
 | 
						|
            shift
 | 
						|
            ;;
 | 
						|
        --pdf)
 | 
						|
            PDF=1
 | 
						|
            shift
 | 
						|
            ;;
 | 
						|
        --txn)
 | 
						|
            TXN=1
 | 
						|
            shift
 | 
						|
            ;;
 | 
						|
        *)
 | 
						|
            if [[ "$1" != -* ]]
 | 
						|
            then 
 | 
						|
                ARGS+=("$1")
 | 
						|
                shift
 | 
						|
            else
 | 
						|
                echo "Error: unknown option $1"
 | 
						|
                exit 1
 | 
						|
            fi
 | 
						|
            ;;
 | 
						|
    esac
 | 
						|
done
 | 
						|
if [[ $HELP = 1 || ${#ARGS} -eq 0 ]]; then usage; exit; fi
 | 
						|
 | 
						|
DEFTIMEACCT=0
 | 
						|
DEFEXPACCT=0
 | 
						|
TEMPLATE="${ARGS[0]}"
 | 
						|
TIMEACCT="${ARGS[1]:-$DEFTIMEACCT}"
 | 
						|
EXPACCT="${ARGS[2]:-$DEFEXPACCT}"
 | 
						|
 | 
						|
# XXX FIXEDEXPS and RATE here, and printf widths below, must be kept synced with TEMPLATE
 | 
						|
FIXEDEXPS=$(python3 -c "print(sum([ 1111, 2222, 333 ]))")
 | 
						|
RATE=444
 | 
						|
TIMELOG=./time.timedot
 | 
						|
#
 | 
						|
 | 
						|
# on mac, use homebrew-installed GNU date
 | 
						|
if [ "$(builtin type -p gdate)" ]; then export date=gdate; else export date=date; fi
 | 
						|
 | 
						|
# the billing date (today)
 | 
						|
YEAR=$($date +%Y)
 | 
						|
MONTH=$($date +%B)
 | 
						|
MON=$($date +%b)
 | 
						|
MM=$($date +%m)
 | 
						|
DD=$($date +%d)
 | 
						|
DAY=$($date +%-d)
 | 
						|
 | 
						|
# The period being billed, as a hledger report period.
 | 
						|
HLEDGERPERIOD='last month'
 | 
						|
# The exact year and month being billed, corresponding to the above.
 | 
						|
YYYYMM=$(hledger -f /dev/null is -p "$HLEDGERPERIOD" | head -1 | cut -d' ' -f3)
 | 
						|
LY=${YYYYMM:0:4}
 | 
						|
LMM=${YYYYMM:5}
 | 
						|
LM=$($date  +%b  --date "$YYYYMM-01")
 | 
						|
 | 
						|
# shellcheck disable=SC2001
 | 
						|
INVOICEBASE=$(basename "$TEMPLATE" | sed -e 's/\..*//')
 | 
						|
INVOICEDATED=$INVOICEBASE$YEAR$LMM
 | 
						|
INVOICEMD=$INVOICEDATED".md"
 | 
						|
INVOICEPDF=$INVOICEDATED".pdf"
 | 
						|
CSS=$INVOICEBASE".css"
 | 
						|
 | 
						|
# Calculate time and reimbursable expenses
 | 
						|
 | 
						|
NUMRE="^[0-9]+([.][0-9]+)?$"
 | 
						|
if [[ $TIMEACCT =~ $NUMRE ]]
 | 
						|
then
 | 
						|
    HRS=$TIMEACCT
 | 
						|
else
 | 
						|
    HRS=$(hledger -f "$TIMELOG" bal "$TIMEACCT" -1 "date:$HLEDGERPERIOD" -N | tail -1 | awk '{print $1}')
 | 
						|
fi
 | 
						|
if [[ $EXPACCT =~ $NUMRE ]]
 | 
						|
then
 | 
						|
    EXP=$EXPACCT
 | 
						|
else
 | 
						|
    EXP=$(hledger bal "$EXPACCT" "date:$HLEDGERPERIOD" amt:'>0' -N --layout=bare | tail -1 | awk '{print $1}')
 | 
						|
fi
 | 
						|
 | 
						|
HRS="${HRS:-0}"
 | 
						|
HRS=$(printf %4s "$HRS")
 | 
						|
EXP=$(printf %5.0f "$EXP")
 | 
						|
AMT=$(python3 -c "print(round( $HRS * $RATE ))")
 | 
						|
AMT=$(printf %5s "$AMT")
 | 
						|
REV=$(python3 -c "print(sum([ $FIXEDEXPS, $AMT ]))")
 | 
						|
REV=$(printf %5s "$REV")
 | 
						|
TOT=$(python3 -c "print(sum([ $FIXEDEXPS, $AMT, $EXP ]))")
 | 
						|
TOT=$(printf %5s "$TOT")
 | 
						|
export  YEAR  MONTH  DAY  LMM  LM  HRS  AMT  REV  EXP  TOT
 | 
						|
 | 
						|
if [[ $MD != 1 && $PDF != 1 ]]; then
 | 
						|
   # print markdown invoice
 | 
						|
   envsubst '$YEAR:$MONTH:$DAY:$LMM:$LM:$HRS:$EXP:$AMT:$TOT' <"$TEMPLATE"
 | 
						|
 | 
						|
else
 | 
						|
    if [[ $MD = 1 ]]; then
 | 
						|
        # save markdown invoice
 | 
						|
        envsubst '$YEAR:$MONTH:$DAY:$LMM:$LM:$HRS:$EXP:$AMT:$TOT' <"$TEMPLATE" >"$INVOICEMD"
 | 
						|
        echo "wrote $INVOICEMD"
 | 
						|
    fi
 | 
						|
    if [[ $PDF = 1 ]]; then
 | 
						|
        # save pdf invoice
 | 
						|
        envsubst '$YEAR:$MONTH:$DAY:$LMM:$LM:$HRS:$EXP:$AMT:$TOT' <"$TEMPLATE" \
 | 
						|
            | pandoc -t html5 --metadata title=" " --css "$CSS" -o "$INVOICEPDF"
 | 
						|
        echo "wrote $INVOICEPDF"
 | 
						|
    fi
 | 
						|
fi
 | 
						|
 | 
						|
if [[ $TXN = 1 ]]; then
 | 
						|
    # generate sample journal entries
 | 
						|
    printf "\n--------------------------------------------------------------------------------\n\n"
 | 
						|
    USTAXRATE=0.28
 | 
						|
    STTAXRATE=0.08
 | 
						|
    CLIENT=$INVOICEBASE
 | 
						|
    USTAX=$(python3 -c "print(round( $REV * $USTAXRATE))")
 | 
						|
    #USTAX=$(printf %5s "$USTAX")
 | 
						|
    STTAX=$(python3 -c "print(round( $REV * $STTAXRATE))")
 | 
						|
    #STTAX=$(printf %5s "$STTAX")
 | 
						|
    TOTTAX=$(python3 -c "print($USTAX + $STTAX)")
 | 
						|
    #TOTTAX=$(printf %5s "$TOTTAX")
 | 
						|
    PTINC=$(python3 -c "print($REV - $TOTTAX)")
 | 
						|
    #PTINC=$(printf %5s "$PTINC")
 | 
						|
 | 
						|
    envsubst '$CLIENT:$YEAR:$MM:$DD:$MON:$LMM:$LM:$REV:$EXP:$TOT:$USTAX:$STTAX:$TOTTAX:$PTINC' <<EOF
 | 
						|
$YEAR-$MM-$DD (${YEAR}${LMM}) $CLIENT | invoice \$$TOT
 | 
						|
    (assets:receivable:$CLIENT:consulting)      \$$REV ; $LM hourly & $MON fixed fees
 | 
						|
    ;(assets:receivable:$CLIENT:reimbursement)  \$$EXP ; $LM reimbursable expenses
 | 
						|
 | 
						|
; $YEAR-$MM-$DD (${YEAR}${LMM}) $CLIENT | payment
 | 
						|
;     ; receive full amount of invoice
 | 
						|
;     assets:bank:checking                \$$TOT
 | 
						|
;     assets:receivable:$CLIENT:reimbursement    \$-$EXP
 | 
						|
;     assets:receivable:$CLIENT:consulting       \$-$REV = $0
 | 
						|
;     ; recognise revenue (cash accounting)
 | 
						|
;     (revenues:$CLIENT)                         \$-$REV
 | 
						|
;     ; estimate tax due, tax-saved-on:  ?, TODO:
 | 
						|
;     (liabilities:tax:us:2021)              \$-$USTAX  ; 28%
 | 
						|
;     (liabilities:tax:st:2021)              \$-$STTAX  ;  8%
 | 
						|
;     ; Total tax:                               \$$TOTTAX  ; 36%
 | 
						|
;     ; Post-tax income:                         \$$PTINC
 | 
						|
 | 
						|
; $YEAR-$MM-$DD save estimated tax from $CLIENT ${YEAR}${LMM}, received $YEAR-$MM-$DD
 | 
						|
;     assets:bank:checking               \$-$TOTTAX
 | 
						|
;     assets:bank:savings:tax:us:2021     \$$USTAX
 | 
						|
;     assets:bank:savings:tax:st:2021     \$$STTAX
 | 
						|
 | 
						|
EOF
 | 
						|
fi
 |