rechnung/rechnung.sh

721 lines
18 KiB
Bash
Executable file

#!/bin/sh -
# file: rechnung.sh
# date: 02.05.2021
# user: bernd@nr18.space
# desc: Liest verschiedene Kunden und Dienstleistungen aus der Datei
# rechnung.ini und erstellt daraus Rechnungen mit Hilfe eines
# Latex-Templates.
set -u
set -e
# ein paar Sicherheitsvorkehrungen
# --------------------------------
OLDIFS=$IFS
IFS='
'
UMASK=077
umask=$UMASK
OLDPATH=$PATH
PATH=/bin:/usr/bin
export PATH
unset -f command
unset -f unalias
\unalias -a
# Variablen
# ---------
PROGRAMM_NAME=$(basename $0)
PROGRAMM_VERSION="0.0.2"
VERBOSITY=0
DEBUG=0
COLORED_OUTPUT=0
RED="$(tput bold)$(tput setaf 1)"
GREEN="$(tput bold)$(tput setaf 2)"
YELLOW="$(tput bold)$(tput setaf 3)"
BLUE="$(tput bold)$(tput setaf 4)"
NORMAL="$(tput bold)$(tput sgr0)"
RECHNUNG_STY_URL="https://github.com/tomka/rechnung"
RECHNUNG_STY_FILE="rechnung.sty"
RECHNUNG_STY_HOME="$HOME/texmf/tex/latex/rechnung"
BASEDIR="$HOME/.rechnung"
KUNDENFILE="rechnung.ini"
TEMPLATEFILE="template.ltx"
LCOFILE="absender.lco"
LCO=
INSTALL_LCO_FILE=0
ANZAHL_RECHNUNGEN=0
READABLES_TO_CHECK=
STARTDIR="$(realpath -s `pwd`)"
TEMPDIR=
OUTPUTDIR="$STARTDIR"
LINE_BUFFER=
KEY_BUFFER=
VALUE_BUFFER=
KUNDE=
ADRESSE=
ANREDE=
BEGLEITTEXT=
ANZAHL=
POSITION=
SHORT=
BETRAG=
LFDNR=
# Benötigte Programme
# -------------------
TR_BIN=$(which tr 2>/dev/null || echo "tr")
BC_BIN=$(which bc 2>/dev/null || echo "bc")
SED_BIN=$(which sed 2>/dev/null || echo "sed")
CUT_BIN=$(which cut 2>/dev/null || echo "cut")
DATE_BIN=$(which date 2>/dev/null || echo "date")
TPUT_BIN=$(which tput 2>/dev/null || echo "tput")
PDFLATEX_BIN=$(which pdflatex 2>/dev/null || echo "pdflatex")
KPSEWHICH_BIN=$(which kpsewhich 2>/dev/null || echo "kpsewhich")
GIT_BIN=$(which git 2>/dev/null || echo "git")
LATEX_BIN=$(which latex 2>/dev/null || echo "latex")
RUBBER_BIN=$(which rubber 2>/dev/null || echo "rubber")
# Nicht auf rubber prüfen; Programm ist optional
# Nicht auf git prüfen; wird nur benötigt, wenn rechnung.sty nicht existiert.
# Nicht auf latex prüfen; wird nur benötigt, wenn rechnung.sty nicht texistiert.
EXECUTABLES_TO_CHECK="$BC_BIN $TR_BIN $SED_BIN $CUT_BIN $TPUT_BIN $DATE_BIN $PDFLATEX_BIN $KPSEWHICH_BIN"
GENPDF=""
if [ -x "$RUBBER_BIN" ]; then
GENPDF="$RUBBER_BIN -d --into $OUTPUTDIR "
else
GENPDF="$PDFLATEX_BIN -halt-on-error -output-directory $OUTPUTDIR "
fi
# Ausgabefunktionen für Terminal
# ------------------------------
print_start() {
if [ "$VERBOSITY" -eq 1 ]; then
if [ "$COLORED_OUTPUT" -eq 1 ]; then
printf "%s[+]%s %s" "$GREEN" "$NORMAL" "$@"
else
printf " Failed"
fi
fi
}
print_middle() {
if [ "$VERBOSITY" -eq 1 ]; then
printf "%s" "$@"
fi
}
print_failed() {
if [ "$VERBOSITY" -eq 1 ]; then
if [ "$COLORED_OUTPUT" -eq 1 ]; then
printf "%s Failed%s\n" "$RED" "$NORMAL"
else
printf " Failed\n"
fi
fi
if [ ! -z "$@" ]; then
print_error "$@"
fi
}
print_ok() {
if [ "$VERBOSITY" -eq 1 ]; then
if [ "$COLORED_OUTPUT" -eq 1 ]; then
printf "%s OK%s\n" "$GREEN" "$NORMAL"
else
printf " OK\n"
fi
fi
}
print_info() {
if [ "$VERBOSITY" -eq 1 ]; then
if [ "$COLORED_OUTPUT" -eq 1 ]; then
echo "${GREEN}[+]$NORMAL $@"
else
echo "[-] $@"
fi
fi
}
print_warn() {
if [ "$VERBOSITY" -eq 1 ]; then
if [ "$COLORED_OUTPUT" -eq 1 ]; then
echo "${YELLOW}[-]$NORMAL $@" 1>&2
else
echo "[-] $@" 1>&2
fi
fi
}
print_error() {
if [ "$COLORED_OUTPUT" -eq 1 ]; then
echo "${RED}[-]$NORMAL Error: $@" 1>&2
else
echo "[-] Error: $@" 1>&2
fi
}
print_head() {
if [ "$VERBOSITY" -eq 1 ]; then
if [ "$COLORED_OUTPUT" -eq 1 ]; then
echo "${BLUE} ========================${NORMAL}"
echo "${BLUE} = $PROGRAMM_NAME v$PROGRAMM_VERSION =${NORMAL}"
echo "${BLUE} ========================${NORMAL}"
else
echo " ========================"
echo " = $PROGRAMM_NAME v$PROGRAMM_VERSION ="
echo " ========================"
fi
fi
}
print_trenner() {
echo "------------------------------------"
}
print_usage() {
cat <<EOF
Usage:
$PROGRAMM_NAME
[-b basedir] Suche hier Dateien, die nicht angegeben wurden
[-d] Debug Modus
[-h] Gibt diese Hilfe aus
[-i] Installiere das Latexpaket rechnung.sty
[-k kundenfile] Pfad zur Datei mit den Kundendaten
[-l lco-file] Pfad zum Latex-Class-Object File
[-o ausgabeverz.] Verzeichnis für die Rechnungen als PDF
[-t templatefile] Pfad zum Latextemplate
[-v] Verbose Modus
[-V] Ausgabe der Programmversion
Für weitere Informationen siehe readme.txt.
EOF
}
print_version() {
echo "$PROGRAMM_NAME version $PROGRAMM_VERSION"
}
# Funktionen zum Beenden
# ----------------------
usage_and_exit() {
print_usage
exit $1
}
clear_and_exit() {
if [ ! $(pwd) = "$STARTDIR" ]; then
print_info "Wechsle nach '$STARTDIR' zurück"
cd $STARTDIR
fi
if [ -d "$TEMPDIR" ]; then
print_info "Entferne temporäres Verzeichnis '$TEMPDIR'"
rm -rf "$TEMPDIR"
fi
msg="Es wurden $ANZAHL_RECHNUNGEN Rechnungen erstellt"
if [ "$COLORED_OUTPUT" -eq 1 ]; then
echo "${BLUE}[*]${NORMAL} $msg"
else
echo "[*] $msg"
fi
exit $1
}
# Diverse Checks
# --------------
check_binary() {
# die funktion prüft, ob das benötigte binariy installiert sind. sollte
# das binary ein symlink auf eine andere datei sein, wird solange den
# symlinks gefolgt, bis die executable gefunden ist. dann wird geprüft,
# ob sich diese im PATH befindet. schlägt die prüfungen fehl, wird das
# programm beendet.
program=$1
print_start "Suche nach $program ..."
if [ ! -x "$program" ]; then
print_failed "Das Programm $program wird benötigt, aber nicht gefunden."
exit 3
else
if [ -h "$program" ]; then
program=$(realpath $program)
print_middle " zeigt auf $program ..."
if ! which "$program" 2>&1 >/dev/null; then
print_failed "$program ist nicht im Pfad"
fi
fi
print_ok
fi
}
check_file() {
# testet, ob die übergebene datei kein symlink ist, existiert und lesbar
# ist. schlägt einer der tests fehl, wird das programm beendet.
file="$*"
print_start "Suche nach Datei $file ..."
if [ -h "$file" ]; then
target=$(realpath "$file")
print_failed "'$file' ist ein symbolischer Link auf '$target'"
exit 4
fi
if test -f "$file" && test -r "$file"; then
print_ok
else
print_failed "Datei '$file' nicht lesbar oder nicht gefunden"
exit 5
fi
}
check_dir() {
# testet, ob das übergebene verzeichnis keine link ist, existiert und
# hinein gewechselt werden darf. schlägt ein test fehl, wird das
# programm beendet.
dir="$*"
print_start "Suche nach Verzeichnis $dir ..."
if [ -h "$dir" ]; then
target=$(realpath "$dir")
print_failed "'$dir' ist ein symbolischer Link auf '$target'"
exit 6
fi
if test -d "$dir" && test -x "$dir"; then
print_ok
else
print_failed "Verzeichnis '$dir' nicht gefunden oder keine Berechtigung"
exit 7
fi
}
check_sty() {
# sucht nach dem paket rechnung.sty. gibt true oder false zurück.
print_start "Suche nach $RECHNUNG_STY_FILE ..."
kpsewhich "$RECHNUNG_STY_FILE" 2>&1 >/dev/null
if [ $? -eq 0 ]; then
print_ok
return 0
else
print_failed
return 1
fi
}
is_option() (
# die funktion überprüft in einer subshell, ob die übergebene zeile ein
# = als trennzeichen enthält. gibt ja oder falsch zurück.
line="$*"
if [ "${line#*=}" != "$line" ]; then
return 0
else
return 1
fi
)
check_rechnunsdaten() {
# die funktion prüft, ob alle variablen belegt sind. ist eine variable
# leer, wird der name der variablen ausgegeben und ein fehler (1) zurück
# gegeben.
response=1
print_start "Prüfe Rechnungsdaten ..."
if test -z "$KUNDE"; then
print_failed "Das Feld Kunde ist leer"
elif test -z "$ADRESSE"; then
print_failed "Das Feld Adresse ist leer"
elif test -z "$ANREDE"; then
print_failed "Das Feld Anrede ist leer"
elif test -z "$ANZAHL"; then
print_failed "Das Feld Anzahl ist leer"
elif test -z "$POSITION"; then
print_failed "Das Feld Position ist leer"
elif test -z "$SHORT"; then
print_failed "Das Feld Short ist leer"
elif test -z "$BETRAG"; then
print_failed "Das Feld Betrag ist leer"
elif test -z "$LFDNR"; then
print_failed "Das Feld LfdNr ist leer"
else
print_ok
response=0
fi
return $response
}
# Funktionen zum setzen von CLI-Variablen
# ---------------------------------------
set_cli_opt() {
# die funktion setzt für einen übergebenen pfad den absoluten pfad ohne
# sich darim zu kümmern, ob er existiert. FIS= sorgt dafür, daß der
# übergebene pfad nicht getrennt wird. sollte nur als subshell
# aufgerufen werden.
IFS=
echo "$(realpath -m -s "$@")"
}
set_abs_path() {
filename="$*"
first=$(echo "$filename" | cut -c1-1)
if [ ! "$first" = "/" ]; then
filename=$(realpath -m "$BASEDIR/$filename")
fi
echo "$filename"
}
# LaTeX-Paket rechnung.sty installieren
# -------------------------------------
install_sty() (
# Clont das Repo von rechnung.sty, baut das Latex-Paket und kopiert es
# nach $HOME/texmf/tex/latex/rechnung. Schlägt einer der Schritte fehl,
# wird clean_and_exit aufgerufen.
## notwendige programm prüfen
for program in "$GIT_BIN" "$LATEX_BIN"; do
check_binary $program
done
## Repo clonen
print_start "Hole Repo von $RECHNUNG_STY_URL ..."
if $GIT_BIN clone $RECHNUNG_STY_URL >/dev/null 2>&1; then
print_ok
cd rechnung
## rechnung.sty kompilieren
print_start "Kompiliere $RECHNUNG_STY_FILE ..."
if $LATEX_BIN rechnung.ins >/dev/null 2>&1; then
print_ok
## mach TEXHOME/rechnung/ kopieren
print_start "Kopiere $RECHNUNG_STY_FILE nach $RECHNUNG_STY_HOME ..."
if [ ! -d "$RECHNUNG_STY_HOME" ]; then
mkdir -p "$RECHNUNG_STY_HOME"
fi
if cp rechnung.sty "$RECHNUNG_STY_HOME"; then
print_ok
print_info "Paket $RECHNUNG_STY_FILE erfolgreich installiert"
else
print_failed "Installation fehlgeschlagen"
clean_and_exit 12
fi
else
print_failed "Kompilieren fehlgeschlagen"
clean_and_exit 11
fi
else
print_failed "Klonen des Repos fehlgeschlagen"
clean_and_exit 10
fi
cd "$TEMPDIR"
)
# Whitespaces entfernen
# in Buffer speichern
# ---------------------
trim_line() {
set -f
set -- "$*"
LINE_BUFFER=$(printf '%s' "$*")
set +f
}
trim_key() {
set -f
set -- "$*"
KEY_BUFFER=$(printf '%s' "$*")
set +f
}
trim_value() {
set -f
set -- "$*"
VALUE_BUFFER=$(printf '%s' "$*")
set +f
}
reset_values() {
print_info "Beginne neue Rechnung"
LINE_BUFFER=""
KEY_BUFFER=""
VALUE_BUFFER=""
KUNDE=""
ADRESSE=""
ANREDE=""
ANZAHL=""
POSITION=""
SHORT=""
BETRAG=""
LFDNR=""
}
set_value() {
key=$(echo "$*" | $CUT_BIN -d "=" -f1)
value=$(echo "$*" | $CUT_BIN -d "=" -f2)
trim_key "$key"
trim_value "$value"
print_info "Setze $key auf $value"
case $KEY_BUFFER in
kunde) KUNDE="$VALUE_BUFFER";;
adresse) ADRESSE="$VALUE_BUFFER";;
anrede) ANREDE="$VALUE_BUFFER";;
anzahl) ANZAHL="$VALUE_BUFFER";;
position) POSITION="$VALUE_BUFFER";;
betrag) BETRAG="$VALUE_BUFFER";;
lfdnr) LFDNR="$VALUE_BUFFER";;
short) SHORT="$VALUE_BUFFER";;
*) print_warn "Unbekanntes Feld: $KEY_BUFFER";;
esac
}
make_invoice() {
# die funktion erstellt in einer subshell die rechnung. schlägt die
# prüfung der rechnungsdaten fehl, kehrt sie zur aufrufenden shell
# zurück. ansonst wird eine kopie des latex-templates erstellt, die
# darin befindlichen platzhalter durch die entsprechenden variablen zu
# ersetzen und das ganze zu kompilieren.
if ! check_rechnunsdaten; then
print_error "Check der Rechnungsdaten fehlgeschlagen"
print_error "Erstelle keine Rechnung"
print_trenner
return
fi
dead_days="14"
if [ "$(date +%u)" -gt 5 ]; then
dead_days="16"
fi
month_ascii=$(date '+%B')
inv_date=$(date '+%Y%m%d')
inv_dead=$(date -d "+${dead_days}days" '+%d.\\,%m.\\,%Y')
rechnungsnummer="$inv_date--$LFDNR"
subject="Rechnung zu $SHORT"
filename="$(date '+%Y%m%d')_Rechnung_${month_ascii}_$(echo "$KUNDE" | $TR_BIN " " "_").ltx"
sedfile="$(basename -s .ltx $filename).sed"
buffer="$KUNDE; $ADRESSE"
anschrift="$(echo "$buffer" | sed 's/; /\\\\\\\\\\\\\\ /g')"
print_info "Erstelle Rechnung: $filename"
{
printf "s!#LCOFILE#!%s!\n" "$LCO"
printf "s!#SUBJECT#!%s!\n" "$subject"
printf "s!#ANREDE#!%s!\n" "$ANREDE"
printf "s!#ANSCHRIFT#!%s!\n" "$anschrift"
printf "s!#RECHNUNGSNUMMER#!%s!\n" "$rechnungsnummer"
printf "s!#ANZAHL#!%s!\n" "$ANZAHL"
printf "s!#POSITION#!%s!\n" "$POSITION"
printf "s!#BETRAG#!%s!\n" "$BETRAG"
printf "s!#DEADLINE#!%s!\n" "$inv_dead"
} >$sedfile
cp "$TEMPLATEFILE" "$filename"
$SED_BIN -i -f "$sedfile" "$filename"
if [ "$DEBUG" -eq 1 ]; then
cp "$filename" "$sedfile" "$OUTPUTDIR"
fi
if ! $GENPDF "$filename" 1>&2 >/dev/null; then
print_error "Kompilierung von $filename fehlgeschlagen"
logfile=$(echo "$OUTPUTDIR/$filename" | sed -e s/ltx/log/)
print_error "$(grep "Error" $logfile)"
else
print_info "Rechnung $filename erfolgreich erstellt"
ANZAHL_RECHNUNGEN=$(expr $ANZAHL_RECHNUNGEN + 1)
fi
if [ -x "$RUBBER_BIN" ]; then
"$RUBBER_BIN" --clean "$filename"
fi
print_trenner
}
parse_line() {
line=$*
first=$(echo "$line" | cut -c1-1)
# leerzeilen ignorieren
if [ "$line" = "" ]; then
return
# kommentarzeilen ignorieren
elif [ "$first" = "#" ]; then
return
# start einer neuen rechnung
elif [ "$line" = "[start]" ]; then
reset_values
# ende einer rechnung
elif [ "$line" = "[ende]" ]; then
make_invoice
# alle anderen zeilen
else
if is_option "$line"; then
set_value "$line"
else
print_warn "Invalid syntax: $line"
fi
fi
}
# --- Programmstart --- #
# --------------------- #
## Nicht als Root laufen lassen
if [ "$(id -u)" -eq 0 ]; then
echo "$PROGRAMM_NAME: Das Skript sollte nicht als Root ausgeführt werden!"
exit 1
fi
## Farbe aktivieren wenn vorhanden
NUMBER_OF_COLORS=$($TPUT_BIN colors)
if [ -n "$NUMBER_OF_COLORS" ] && [ "$NUMBER_OF_COLORS" -ge 8 ]; then
COLORED_OUTPUT=1
fi
## CLI Optionen auswerten
while getopts b:dhik:l:o:t:vV opt
do
case $opt in
b)
BASEDIR=$(set_cli_opt "$OPTARG")
;;
d)
DEBUG=1
;;
h)
usage_and_exit 0
;;
i)
INSTALL_LCO_FILE=1
;;
k)
KUNDENFILE=$(set_cli_opt "$OPTARG")
;;
l)
LCOFILE=$(set_cli_opt "$OPTARG")
;;
o)
OUTPUTDIR=$(set_cli_opt "$OPTARG")
;;
t)
TEMPLATEFILE=$(set_cli_opt "$OPTARG")
;;
v)
VERBOSITY=1
;;
V)
print_version
;;
*)
usage_and_exit 2
;;
esac
done
print_head
## Die nötigen Programme und rechnung.sty testen
for program in $EXECUTABLES_TO_CHECK; do
check_binary $program
done
## relative pfade in den variablen LCOFILE, KUNDENFILE und TEMPLATEFILE wenn
## nötig in absolute pfade umwandeln. -- TODO: das geht sicher besser.
LCOFILE=$(set_abs_path "$LCOFILE")
KUNDENFILE=$(set_abs_path "$KUNDENFILE")
TEMPLATEFILE=$(set_abs_path "$TEMPLATEFILE")
## verzeichnisse testen
check_dir "$BASEDIR"
check_dir "$OUTPUTDIR"
## die nötigen Dateien testen und die Variable LCO setzen
for file in "$LCOFILE" "$KUNDENFILE" "$TEMPLATEFILE"; do
check_file "$file"
done
LCO=$(basename "$LCOFILE" .lco)
## temporäres Verzeichnis erstellen und hineinwechseln
TEMPDIR=$(mktemp -d)
if [ ! $? -eq 0 ]; then
print_error "Erstellen der temp. Verzeichnis fehlgeschlagen"
exit 8
else
cd "$TEMPDIR"
print_info "Wechsle nach $(pwd)"
## prüfen, ob rechnung.sty installiert ist
if ! check_sty; then
if [ $INSTALL_LCO_FILE -eq 0 ]; then
print_error "Verwenden sie -i um $RECHNUNG_STY_FILE zu installieren"
clear_and_exit 9
else
print_info "Versuche $RECHNUNG_STY_FILE zu installieren"
install_sty
fi
fi
# Kundendatei einlesen und die einzelnen Zeilen an den Zeilenparser
# weiterreichen.
while IFS= read -r rawline
do
trim_line "$rawline"
parse_line "$LINE_BUFFER"
done < "$KUNDENFILE"
fi
# aufräumen
clear_and_exit 0