Skip to content

Instantly share code, notes, and snippets.

@linuxcaffe
Created November 14, 2021 02:45
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save linuxcaffe/4d549c871c174a0b7756d8a1682cb00f to your computer and use it in GitHub Desktop.
Save linuxcaffe/4d549c871c174a0b7756d8a1682cb00f to your computer and use it in GitHub Desktop.
hledger+pandoc invoice generator
/* https://martinbetz.eu/articles/pandoc-invoices */
@charset "utf-8";
body {
/* font-size: 10.5pt; */
/* font-family: */
/* "Avenir Next", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", */
/* "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; */
hyphens: auto;
height: 250mm; /* 280 - 10 (top) - 20 (bottom) */
line-height: 140%;
margin: 0;
padding: 0;
}
code {
font-family: "Source Sans Code", Courier New, Courier, monospace;
margin-left: 1pt;
/* font-size:12pt; */
}
a {
color: black;
margin-left: 1pt;
}
h1 {
font-size: 20pt;
margin-top: 10mm;
padding-top:0;
/* margin-top: 6pt; */
/* margin-bottom: 0; */
}
h2 {
font-size: 16pt;
/* margin-top: 20pt; */
margin-top: 10mm;
/* font-weight: normal; */
/* margin-top: 0; */
/* margin-bottom: 20pt; */
}
p {
width: 100%;
}
p:first-of-type {
text-align: center;
font-size: 9pt;
word-spacing: 1pt;
}
p:nth-of-type(2) {
margin-top: 10mm;
}
p:nth-of-type(3) {
text-align: center;
font-weight:bold;
font-size:12pt;
}
/* p:nth-last-of-type(3) { */
/* margin-top: 10mm; */
/* } */
/* p:last-of-type { */
/* text-align: center; */
/* font-size: 9pt; */
/* position: absolute; */
/* bottom: 2mm; */
/* margin-bottom: 0; */
/* padding-bottom: 0; */
/* color: #444; */
/* } */
table {
width: 100%;
}
table:nth-of-type(1) {
border: 1px solid black;
padding: 5pt;
}
table:nth-of-type(1) td {
border-top: 1px solid #eee;
}
table:nth-of-type(1) tr:nth-last-of-type(1) {
font-weight: bold;
}
table:nth-of-type(1) tr:nth-last-of-type(1) td {
/* border-top: 1px solid black; */
padding-top: 1em;
}
/* table:nth-of-type(1) td:nth-of-type(2) { */
/* text-align: center; */
/* } */
hr {
border: 1px solid #eee;
}
hr:last-of-type {
position: absolute;
bottom: 14mm;
width: 100%;
}
figure {
margin: 0;
}

papersize: letter margin-left: 20mm margin-right: 25mm margin-top: 20mm margin-bottom: 20mm ...

{ width=100mm }
My Name | +1 (310) 111 1111 | my@email | My address

Client
Client's
address

$MONTH $DAY, $YEAR

Invoice ${YEAR}${LMM}code

Description Rate Qty Total
Systems reliability engineering $ 1111 $ 1111
On-call monitoring & tech support $ 2222 $ 2222
Contractor/vendor management $ 333 $ 333
Custom SW development & maintenance ($LM) $ 444 $HRS $ $AMT
Reimbursable expenses ($LM) $ $EXP
 
Total due $ $TOT

Terms: Now due. Your business is appreciated, thank you!

INVOICECMD=./mkinvoice client.md work.client.dev assets:receivable:client:reimbursement
client-dry:
$(INVOICECMD)
client:
$(INVOICECMD) -m -p
@read -p "press enter to commit, or ctrl-c to cancel: "
$(INVOICECMD) -c
#!/usr/bin/env bash
# shellcheck disable=SC2016
set -e
PROG=$(basename "$0")
DEFTIMEACCT=0
DEFEXPACCT=0
function usage() {
cat <<EOS
--------------------------------------------------------------------------------
Make a md or pdf invoice for hledger-reported time and expenses last month,
from a pandoc markdown template and similarly-named .css file.
Requires awk, GNU date, envsubst, python3, sed, tail, pandoc, hledger 20210808+
Usage:
$PROG [TEMPLATEFILE [TIMEACCT|HRS [EXPACCT|AMT]]]
[-m|--markdown] [-p|--pdf] [-f|--force] [-c|--commit]
With no arguments, show this help.
With no flags, make a markdown invoice from TEMPLATEFILE on stdout for preview.
Additional arguments set the time and expense accounts for hledger to
query, or if numeric, the time and expense amounts directly.
With flags,
-m|--markdown save a markdown invoice if input files are newer
-p|--pdf save a pdf if input files are newer, & open folder for preview
-f|--force force invoice generation even if input files are older
-c|--commit git commit any saved markdown/pdf invoices
EOS
}
ARGS=()
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-h|--help)
HELP=1
shift
;;
-m|--markdown)
MD=1
shift
;;
-p|--pdf)
PDF=1
shift
;;
-f|--force)
FORCE=1
shift
;;
-c|--commit)
COMMIT=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
TEMPLATE="${ARGS[0]}"
TIMEACCT="${ARGS[1]:-$DEFTIMEACCT}"
EXPACCT="${ARGS[2]:-$DEFEXPACCT}"
# when changing this, hledger and date may need different values
HLEDGERPERIOD='last month'
DATECMDPERIOD='last month'
# HLEDGERPERIOD='2021-08'
# DATECMDPERIOD='2021-08-01'
NUMRE="^[0-9]+([.][0-9]+)?$"
if [[ $TIMEACCT =~ $NUMRE ]]
then
HRS=$TIMEACCT
else
HRS=$(hledger -f~/adm/time.journal 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 --commodity-column | tail -1 | awk '{print $1}')
fi
# on mac, use homebrew-installed GNU date
if [ "$(builtin type -p gdate)" ]; then export date=gdate; else export date=date; fi
# XXX FIXEDEXPS, RATE, other vars below, and printf widths must be kept synced with TEMPLATE
FIXEDEXPS=$(python3 -c "print(sum([ 1111, 2222, 333 ]))")
RATE=444
YEAR=$($date +%Y)
MONTH=$($date +%B)
DAY=$($date +%-d)
LM=$($date +%b --date "$DATECMDPERIOD")
LMM=$($date +%m --date "$DATECMDPERIOD")
# shellcheck disable=SC2001
INVOICEBASE=$(basename "$TEMPLATE" | sed -e 's/\..*//')
INVOICEDATED=$INVOICEBASE$YEAR$LMM
INVOICEMD=$INVOICEDATED".md"
INVOICEPDF=$INVOICEDATED".pdf"
CSS=$INVOICEBASE".css"
HRS="${HRS:-0}"
HRS=$(printf %4s "$HRS")
EXP=$(printf %5.0f "$EXP")
AMT=$(python3 -c "print(round( $HRS * $RATE ))")
AMT=$(printf %5s "$AMT")
TOT=$(python3 -c "print(sum([ $FIXEDEXPS, $AMT, $EXP ]))")
TOT=$(printf %5s "$TOT")
export YEAR MONTH DAY LMM LM HRS EXP AMT TOT
if [[ $MD != 1 && $PDF != 1 ]];
then
envsubst '$YEAR:$MONTH:$DAY:$LMM:$LM:$HRS:$EXP:$AMT:$TOT' <"$TEMPLATE"
else
if [[ $MD = 1 ]];
then
if [[ $FORCE = 1 || $TEMPLATE -nt $INVOICEMD ]]
then
envsubst '$YEAR:$MONTH:$DAY:$LMM:$LM:$HRS:$EXP:$AMT:$TOT' <"$TEMPLATE" >"$INVOICEMD"
echo "wrote $INVOICEMD"
fi
fi
if [[ $PDF = 1 ]];
then
if [[ $FORCE = 1 || $TEMPLATE -nt $INVOICEMD || $CSS -nt $INVOICEMD ]]
then
envsubst '$YEAR:$MONTH:$DAY:$LMM:$LM:$HRS:$EXP:$AMT:$TOT' <"$TEMPLATE" \
| pandoc -t html5 --metadata title=" " --css "$CSS" -o "$INVOICEPDF"
echo "wrote $INVOICEPDF"
fi
if [[ $COMMIT != 1 ]]
then
open .
fi
fi
if [[ $COMMIT = 1 ]]
then
git add "$INVOICEMD" "$INVOICEPDF" && \
git commit -m "$INVOICEDATED" -- "$INVOICEMD" "$INVOICEPDF" && \
echo "committed:" && git show --stat
fi
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment