rechnung/rechnung.sh

603 lines
14 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='
'
unset -f command
unset -f unalias
\unalias -a
OLDPATH=$PATH
PATH=/bin:/usr/bin
export PATH
# 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 "
else
GENPDF="$PDFLATEX_BIN -halt-on-error "
fi
# Variablen definieren
# --------------------
PROGRAMM_NAME=$(basename "$0")
PROGRAMM_VERSION="0.0.1"
VERBOSITY=1
STY_HOME="$HOME/texmf/tex/latex"
STY_RECHNUNG="rechnung.sty"
RECHNUNGSFILE="rechnung.ini"
TEMPLATEFILE="template.ltx"
LCOFILE="absender.lco"
LCO=$(basename $LCOFILE .lco)
INSTALL_LCO_FILE=0
SEDFILE="sed.txt"
LINE_BUFFER=""
KEY_BUFFER=""
VALUE_BUFFER=""
KUNDE=""
ADRESSE=""
ANREDE=""
ANZAHL=""
POSITION=""
EINLEITUNG=""
SHORT=""
BETRAG=""
LFDNR=""
# Farben definieren
# -----------------
NUMBER_OF_COLORS=$($TPUT_BIN colors)
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)"
# Ausgabefunktionen für Terminal
# ------------------------------
print_start() {
msg=$*
if [ "$VERBOSITY" -eq 0 ]; then
return
else
if [ "$COLORED_OUTPUT" -eq 1 ]; then
printf "%s[+]%s %s" "$GREEN" "$NORMAL" "$msg"
else
printf " Failed"
fi
fi
}
print_failed() {
msg=$*
if [ "$VERBOSITY" -eq 0 ]; then
return
else
if [ "$COLORED_OUTPUT" -eq 1 ]; then
printf "%s Failed%s\n" "$RED" "$NORMAL"
else
printf " Failed\n"
fi
if [ -n "$msg" ]; then
print_error "$msg"
fi
fi
}
print_ok() {
if [ "$VERBOSITY" -eq 0 ]; then
return
else
if [ "$COLORED_OUTPUT" -eq 1 ]; then
printf "%s OK%s\n" "$GREEN" "$NORMAL"
else
printf " OK\n"
fi
fi
}
print_deko() {
msg=$*
if [ "$VERBOSITY" -eq 0 ]; then
return
else
if [ "$COLORED_OUTPUT" -eq 1 ]; then
echo "${BLUE}${msg}${NORMAL}"
else
echo "$msg"
fi
fi
}
print_info() {
msg=$*
if [ "$VERBOSITY" -eq 0 ]; then
return
else
if [ "$COLORED_OUTPUT" -eq 1 ]; then
echo "${GREEN}[+]$NORMAL $msg"
else
echo "[-] $msg"
fi
fi
}
print_warn() {
msg=$*
if [ "$VERBOSITY" -eq 0 ]; then
return
else
if [ "$COLORED_OUTPUT" -eq 1 ]; then
echo "${YELLOW}[-]$NORMAL $msg"
else
echo "[-] $msg"
fi
fi
}
print_error() {
msg=$*
if [ "$COLORED_OUTPUT" -eq 1 ]; then
echo "${RED}[-]$NORMAL $msg"
else
echo "[-] $msg"
fi
}
failed_and_exit() {
print_failed
exit "$*"
}
# Diverse Checks
# --------------
check_readable() {
# testet, ob die übergebene datei kein symlink ist, existiert und lesbar
# ist. schlägt einer der tests fehl, wird das programm beendet.
print_start "Datei $1 ist lesbar ..."
if test -h "$1"; then
target=$(get_link_target "$1")
print_failed "$1 ist ein symbolischer Link auf $target"
exit 1
fi
if test -f "$1" && test -r "$1"; then
print_ok
else
failed_and_exit 1
fi
}
check_sty() (
# sucht in einer subshell nach dem paket rechnung.sty. gibt true oder
# false zurück.
print_start "Suche nach $STY_RECHNUNG ..."
if $KPSEWHICH_BIN "$STY_RECHNUNG" > /dev/null 2>&1 ; then
print_ok
return 0
else
print_failed
return 1
fi
)
check_binaries() (
# die funktion prüft in einer subshell, ob alle benötigten binaries
# installiert sind. sollte das binary ein symlink auf eine ander datei
# sein, wird geprüft, ob sich diese im PATH befindet und nicht ebenfalls
# ein symlink ist. schlägt eine der prüfungen fehl, wird das programm
# beendet.
program=
binaries=$*
for program in $binaries; do
print_start "Suche nach $program ..."
if test ! -x "$program"; then
print_failed "Das Programm $program wird benötigt, aber nicht gefunden."
quit 3
else
if [ -h "$program" ]; then
program=$(get_link_target "$program")
fi
if ! which "$program" > /dev/null 2>&1 ; then
print_failed "$program ist nicht im Pfad"
fi
print_ok
fi
done
)
get_link_target() (
# die funktion bekommt einen symbolischen link und folgt ihm bis zum
# ende. mit dem echo-befehl wird das ziel an die aufrufende funktion
# zurück gegeben.
program=$1
while [ -h "$program" ]; do
program=$(readlink "$program")
done
echo "$program"
)
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 überprüft in einer subshell, ob alle variablen belegt
# sind. ist eine variable leer, wird der name der variablen ausgegeben
# und ein fehler (1) zurück gegeben.
print_start "Prüfe Rechnungsdaten ..."
if test -z "$KUNDE"; then
print_failed "Das Feld Kunde ist leer"
return 1
elif test -z "$ADRESSE"; then
print_failed "Das Feld Adresse ist leer"
return 1
elif test -z "$ANREDE"; then
print_failed "Das Feld Anrede ist leer"
return 1
elif test -z "$ANZAHL"; then
print_failed "Das Feld Anzahl ist leer"
return 1
elif test -z "$POSITION"; then
print_failed "Das Feld Position ist leer"
return 1
elif test -z "$EINLEITUNG"; then
print_failed "Das Feld Einleitung ist leer"
return 1
elif test -z "$SHORT"; then
print_failed "Das Feld Short ist leer"
return 1
elif test -z "$BETRAG"; then
print_failed "Das Feld Betrag ist leer"
return 1
elif test -z "$LFDNR"; then
print_failed "Das Feld LfdNr ist leer"
return 1
else
print_ok
return 0
fi
)
# 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
}
# LaTeX-Paket rechnung.sty installieren
# -------------------------------------
install_sty() (
old_pwd=$(pwd)
check_binaries "$GIT_BIN $LATEX_BIN"
print_start "Hole Repo von $STY_URL ..."
if $GIT_BIN clone "$STY_URL" >/dev/null 2>&1; then
print_ok
cd rechnung
mkdir -p "$STY_HOME/rechnung/"
print_start "Kompiliere $STY_RECHNUNG ..."
if $LATEX_BIN rechnung.ins >/dev/null 2>&1; then
print_ok
print_start "Kopiere $STY_RECHNUNG nach $STY_HOME/rechnung ..."
if cp rechnung.sty "$STY_HOME/rechnung/"; then
print_ok
else
failed_and_exit 3
fi
cd "$old_pwd"
rm -rf rechnung/
print_info "Paket $STY_RECHNUNG erfolgreich installiert"
else
print_error "Installation fehlgeschlagen"
exit 4
fi
else
print_error "Klonen des Repos fehlgeschlagen"
exit 5
fi
)
# wenn ein neuer Kunde beginnt, alle Variablen neu initialisieren
# ---------------------------------------------------------------
reset_values() {
print_info "Beginne neue Rechnung"
LINE_BUFFER=""
KEY_BUFFER=""
VALUE_BUFFER=""
KUNDE=""
ADRESSE=""
ANREDE=""
ANZAHL=""
POSITION=""
EINLEITUNG=""
SHORT=""
BETRAG=""
LFDNR=""
}
set_value() {
key=$(echo "$*" | $CUT_BIN -d "=" -f1)
value=$(echo "$*" | $CUT_BIN -d "=" -f2)
trim_key "$key"
trim_value "$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";;
einleitung) EINLEITUNG="$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
return 1
fi
dead_days="14"
if [ "$(date +%u)" -gt 5 ]; then
dead_days="16"
fi
# Bei der Ersetzung unten wird jedes ';' durch genau ein '\' ersetzt. Daher ist die Anzahl der ';' wichtig.
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"
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" "$(echo "$buffer" | sed 's/;/\\\\/g')"
printf "s!#RECHNUNGSNUMMER#!%s!\n" "$rechnungsnummer"
printf "s!#ANZAHL#!%s!\n" "$ANZAHL"
printf "s!#POSITION#!%s!\n" "$POSITION"
printf "s!#EINLEITUNG#!%s!\n" "$EINLEITUNG"
printf "s!#BETRAG#!%s!\n" "$BETRAG"
printf "s!#DEADLINE#!%s!\n" "$(echo "$inv_dead" | sed 's/;/\\\\/g')"
} >$SEDFILE
cp "$TEMPLATEFILE" "$filename"
$SED_BIN -i -f "$SEDFILE" "$filename"
if ! $GENPDF "$filename" 1>&2 >/dev/null; then
print_error "Kompilierung von $filename fehlgeschlagen"
logfile=$(echo "$filename" | sed -e s/ltx/log/)
print_error "$(grep "Error" "$logfile")"
else
print_info "Rechnung $filename erfolgreich erstellt"
fi
if [ -x "$RUBBER_BIN" ]; then
"$RUBBER_BIN" --clean "$filename"
fi
)
proced_ini_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
}
display_help() (
echo "Usage: $PROGRAMM_NAME [-f rechnungsfile] [-h] [-l lco-file]"
)
display_version() (
echo "$PROGRAMM_NAME version $PROGRAMM_VERSION"
)
quit() {
printf "\n"
printf "%s" "$BLUE"
printf "[-] Exit%s\n" "$NORMAL"
exit 1
}
# --- Programmstart --- #
# --------------------- #
# CLI Optionen auswerten
while getopts f:hi:l:qv opt
do
case $opt in
f) RECHNUNGSFILE=$OPTARG;;
h) display_help; exit 0;;
i) INSTALL_LCO_FILE=1;;
l) LCOFILE=$OPTARG;;
q) VERBOSITY=0;;
v) display_version; exit 0;;
*) display_help; exit 1;;
esac
done
# Farbe aktivieren wenn vorhanden
if [ -n "$NUMBER_OF_COLORS" ] && [ "$NUMBER_OF_COLORS" -ge 8 ]; then
COLORED_OUTPUT=1
fi
print_deko " ========================"
print_deko " = $PROGRAMM_NAME v$PROGRAMM_VERSION ="
print_deko " ========================"
# Verschiedene Basic Checks
if [ "$(id -u)" -eq 0 ]; then
print_error "$0: Das Skript darf nicht als Root ausgeführt werden."
exit 1
fi
check_binaries "$EXECUTABLES_TO_CHECK"
check_readable "$RECHNUNGSFILE"
check_readable "$TEMPLATEFILE"
check_readable "$LCOFILE"
if ! check_sty; then
print_warn "Verwenden sie -i um $STY_RECHNUNG zu installieren"
if [ $INSTALL_LCO_FILE -eq 0 ]; then
exit 7
else
install_sty
fi
fi
# Kundendatei einlesen und rechnungen erstellen
while IFS= read -r rawline
do
trim_line "$rawline"
proced_ini_line "$LINE_BUFFER"
done < "$RECHNUNGSFILE"