;examples: invoicing: new invoice script example

This commit is contained in:
Simon Michael 2021-12-04 10:41:41 -10:00
parent 7c4f5dc2bf
commit 7edcf77eae
6 changed files with 411 additions and 0 deletions

View File

@ -0,0 +1,79 @@
Scripts adapted from a real-world setup, not guaranteed to be current or working.
Example:
Show invoice preview:
```
$ ./invoice abinvoice.tmpl.md client.ab.dev 200
---
papersize: letter
margin-left: 20mm
margin-right: 25mm
margin-top: 20mm
margin-bottom: 20mm
...
![](logo.jpg){ width=100mm }\
Joe Consultant | +1 (111) 111 1111 | joe@example.com | 500 Done Dr. #1, Work Ville, CA 10000, USA
Carl Client\
AB Inc.\
PO Box 11111\
CA 20000\
December 4, 2021
# Invoice 202111cw
| 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 (Nov) | $ 444 | 2.00 | $ 888 |
| Reimbursable expenses (Nov) | | | $ 200 |
|   | | | |
| Total due | | | $ 4754 |
| | | | |
Terms: Now due. Your business is appreciated, thank you!
```
Generate markdown and PDF invoices and sample journal entries:
```
$ ./invoice abinvoice.tmpl.md client.ab.dev 200 --md --pdf --txn
wrote abinvoice202111.md
Loading pages (1/6)
Counting pages (2/6)
Resolving links (4/6)
Loading headers and footers (5/6)
Printing pages (6/6)
Done
wrote abinvoice202111.pdf
--------------------------------------------------------------------------------
2021-12-04 (202111) abinvoice | invoice $ 4754
(assets:receivable:abinvoice:consulting) $ 4554 ; Nov hourly & Dec fixed fees
;(assets:receivable:abinvoice:reimbursement) $ 200 ; Nov reimbursable expenses
; 2021-12-04 (202111) abinvoice | payment
; ; receive full amount of invoice
; assets:bank:checking $ 4754
; assets:receivable:abinvoice:reimbursement $- 200
; assets:receivable:abinvoice:consulting $- 4554 = ./invoice
; ; recognise revenue (cash accounting)
; (revenues:abinvoice) $- 4554
; ; estimate tax due, tax-saved-on: ?, TODO:
; (liabilities:tax:us:2021) $-1275 ; 28%
; (liabilities:tax:st:2021) $-364 ; 8%
; ; Total tax: $1639 ; 36%
; ; Post-tax income: $2915
; 2021-12-04 save estimated tax from abinvoice 202111, received 2021-12-04
; assets:bank:checking $-1639
; assets:bank:savings:tax:us:2021 $1275
; assets:bank:savings:tax:st:2021 $364
```

View File

@ -0,0 +1,117 @@
/* 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;
}

View File

@ -0,0 +1,33 @@
---
papersize: letter
margin-left: 20mm
margin-right: 25mm
margin-top: 20mm
margin-bottom: 20mm
...
![](logo.jpg){ width=100mm }\
Joe Consultant | +1 (111) 111 1111 | joe@example.com | 500 Done Dr. #1, Work Ville, CA 10000, USA
Carl Client\
AB Inc.\
PO Box 11111\
CA 20000\
$MONTH $DAY, $YEAR
# Invoice ${YEAR}${LMM}cw
| 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!

View File

@ -0,0 +1,180 @@
#!/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
#
# if changing this period, hledger and date may need different values
HLEDGERPERIOD='last month'
DATECMDPERIOD='last month'
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
# on mac, use homebrew-installed GNU date
if [ "$(builtin type -p gdate)" ]; then export date=gdate; else export date=date; fi
YEAR=$($date +%Y)
MONTH=$($date +%B)
MON=$($date +%b)
MM=$($date +%m)
DD=$($date +%d)
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")
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

View File

@ -0,0 +1,2 @@
2021-11-15
client.ab.dev .... ....