rechnung/rechnung.py

467 lines
14 KiB
Python
Executable File

#!/usr/bin/python3
# file: rechnung.py
# date: 03.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.
import os
import sys
import locale
import subprocess
from datetime import date, timedelta
class DISPLAY():
'''
Die Klasse für die Ausgabe von Logmeldungen im Terminal. Schaut, ob und
worauf die Environment Variable "COLORTERM" gesetzt ist, und gibt
entsprechend dem Ergebnis farbige Ausgabem.
'''
def __init__(self):
self.red = '\x1B[31m'
self.green = '\x1B[32m'
self.yellow = '\x1B[33m'
self.magenta = '\x1B[35m'
self.reset = '\x1B[39m'
self.colorify = False
self.terminal_has_color()
def terminal_has_color(self):
try:
if os.environ["COLORTERM"] in ("256", "truecolor"):
self.colorify = True
except KeyError:
pass
def TRUE(self):
'''
'''
if self.colorify == True:
print(f"{self.green}OK{self.reset}")
else: print("OK")
def FALSE(self):
'''
'''
if self.colorify == True:
print(f"{self.red}Failed{self.reset}")
else: print("Failed")
def DEKO(self, msg, end='\n'):
'''
param 1: string
param 2: string
'''
if self.colorify == True:
print(f"{self.magenta}{msg}{self.reset}", end=end)
else: print(f"{msg}", end=end)
def INFO(self, msg, end='\n'):
'''
param 1: string
param 2: string
'''
if self.colorify == True:
print(f"{self.green}[+]{self.reset} {msg}", end=end, flush=True)
else: print(f"[+] {msg}", end=end)
def WARN(self, msg, end='\n'):
'''
param 1: string
'''
if self.colorify == True:
print(f"{self.yellow}[-]{self.reset} {msg}", end=end)
else: print(f"[-] {msg}", end=end)
def ERROR(self, msg, end='\n'):
'''
param 1: string
'''
if self.colorify == True:
print(f"{self.red}[-]{self.reset} {msg}", end=end)
else: print(f"[-] {msg}", end=end)
class KUNDE():
'''
Die Klasse dient lediglich zum Halten der Informationen zu einem
Rechnungskunden.
'''
def __init__(self):
self.kunde = ""
self.adresse = ""
self.anrede = ""
self.anzahl = ""
self.position = ""
self.short = ""
self.betrag = ""
self.lfdnr = ""
self.error = False
class RECHNUNG():
'''
'''
def __init__(self):
self.programm_name = "rechnung.py"
self.programm_version = "0.0.1"
self.rechnungsfile = "rechnung.ini"
self.templatefile = "template.ltx"
self.texhome = "/".join((os.environ["HOME"], "texmf"))
self.compiler = ""
self.D = DISPLAY()
locale.setlocale(locale.LC_TIME, "de_DE.utf8")
def print_header(self):
'''
'''
self.D.DEKO(" =============================")
self.D.DEKO(" | rechnung.py v{} |".format(self.programm_version))
self.D.DEKO(" =============================")
def check_readable(self, filename):
'''
param 1: string
'''
self.D.INFO("Teste, ob {} lesbar ist ... ".format(filename), end="")
response = os.access(filename, os.R_OK)
if response == True:
self.D.TRUE()
else:
self.D.FALSE
sys.exit(1)
def check_program(self, program):
'''
param 1: string
returns: boolean
'''
self.D.INFO(f"Teste, ob {program} installiert ist ... ", end="")
response = subprocess.run(["which", program], capture_output=True)
if response.returncode == 0:
self.D.TRUE()
self.compiler = program
return True
else:
self.D.FALSE()
return False
def parse_buffer(self, buffer):
'''
param 1: liste
'''
for line in buffer:
line = line.strip()
if line == "" or line.startswith("#"):
pass
elif line.startswith("[start]"):
self.K = KUNDE()
elif line.startswith("[ende]"):
self.make_invoice()
del(self.K)
else: self.set_customer_values(line)
def set_customer_values(self, line):
'''
param 1: string
'''
if line.find("=") == -1:
self.D.WARN(f"Ungültige Syntax: {line}")
return
key, value = line.split("=")
if key == "kunde":
self.K.kunde = value.strip()
elif key == "adresse":
self.K.adresse = value.strip()
elif key == "anrede":
self.K.anrede = value.strip()
elif key == "anzahl":
self.K.anzahl = value.strip()
elif key == "position":
self.K.position = value.strip()
elif key == "short":
self.K.short = value.strip()
elif key == "betrag":
self.K.betrag = value.strip()
elif key == "lfdnr":
self.K.lfdnr = value.strip()
else:
self.D.WARN(f"Fehler beim Parsen von {self.K.kunde}")
self.D.WARN(f"Ungültige Option: {key}")
self.K.error = True
def make_invoice(self):
'''
'''
buffer = None
self.D.INFO(f"Erstelle Rechung für Kunde {self.K.kunde}")
for i in self.K.__dict__:
if self.K.__dict__[i] == "":
self.D.WARN(f"Leeres Feld: {i}")
self.K.error = True
if self.K.error is True:
self.D.WARN(f"Kunde enthält einen Error. Erstelle keine Rechnung.")
return
buffer = self.get_file_content(self.templatefile)
if buffer is None:
self.D.ERROR(f"Kann {self.templatefile} nicht lesen. Exit")
sys.exit(4)
latex = self.replace(buffer)
output_filename = self.build_output_filename()
if output_filename == "":
self.D.ERROR("Kann Dateinamen nicht erstellen")
sys.exit(5)
with open(output_filename, "w") as fd:
for i in latex:
fd.write(i)
fd.close()
response = subprocess.run([self.compiler, output_filename], capture_output=True)
if response.returncode != 0:
self.D.ERROR(f"Fehler beim Compilieren für {self.K.kunde}")
else:
subprocess.run([self.compiler, output_filename], capture_output=True)
self.D.INFO(f"Rechnung für {self.K.kunde} erfolgreich erstellt.")
def replace(self, buffer):
'''
'''
latex = []
for line in buffer:
if line.find("#ANSCHRIFT#") != -1:
anschrift = self.build_anschrift()
line = line.replace("#ANSCHRIFT#", anschrift)
elif line.find("#RECHNUNGSNUMMER#") != -1:
invoice_number = self.build_invoice_number()
line = line.replace("#RECHNUNGSNUMMER#", invoice_number)
elif line.find("#SUBJECT#") != -1:
subject = self.build_subject()
line = line.replace("#SUBJECT#", subject)
elif line.find("#ANREDE#") != -1:
line = line.replace("#ANREDE#", self.K.anrede)
elif line.find("#DEADLINE#") != -1:
deadline = self.build_deadline()
line = line.replace("#DEADLINE#", deadline)
if line.find("#ANZAHL#") != -1:
line = line.replace("#ANZAHL#", self.K.anzahl)
if line.find("#POSITION#") != -1:
line = line.replace("#POSITION#", self.K.position)
if line.find("#BETRAG#") != -1:
line = line.replace("#BETRAG#", self.K.betrag)
latex.append(line)
return latex
def build_output_filename(self):
'''
returns: string
'''
filename = ""
kunde = self.K.kunde.replace(" ", "_")
today = date.today().strftime("%Y%m%d")
month = date.today().strftime("%B")
filename = "_".join((today, "Rechnung", month, kunde))
filename = ".".join((filename, "ltx"))
return filename
def build_anschrift(self):
'''
returns: string
'''
anschrift = ""
anschrift = self.K.kunde.strip()
adressliste = self.K.adresse.split(";")
for i in adressliste:
anschrift = "\\\\".join((anschrift, i.strip()))
return anschrift
def build_subject(self):
'''
returns: string
'''
try:
subject = " ".join(("Rechnung zu", self.K.short.strip()))
except Exception as e:
self.D.ERROR("Fehler beim Erstellen des Subjects")
self.D.ERROR(f"Error: {e}")
self.K.error = True
subject = ""
return subject
def build_invoice_number(self):
'''
returns: string
'''
try:
today = date.today().strftime("%Y%m%d")
invoice_number = "-".join((today, self.K.lfdnr))
except Exception as e:
self.D.ERROR("Fehler beim Erstellen der Rechnungsnummer")
self.D.ERROR(f"Error: {e}")
self.K.error = True
invoice_number = ""
return invoice_number
def build_deadline(self):
'''
returns: string
'''
try:
delta = timedelta(days=14)
deadline = date.today()
deadline = deadline + delta
tupel = date.timetuple(deadline)
delta = timedelta(days=1)
while tupel.tm_wday in (5, 6):
deadline = deadline + delta
tupel = date.timetuple(deadline)
deadline = str(deadline.strftime("%d.%m.%Y"))
except Exception as e:
self.D.ERROR("Fehler beim Erstellen der Deadline")
self.D.ERROR(f"Error: {e}")
self.K.error = True
deadline = ""
return deadline
def get_file_content(self, filename):
'''
param 1: string
returns: liste
'''
buffer = None
with open(filename, "r") as fd:
buffer = fd.readlines()
fd.close()
return buffer
def check_sty(self):
'''
Rechnung.sty steht nicht als Paket zur Verfügung. Es muß folglich
von Hand in den Suchpfad von Latex kopiert werden. Die Suche erfolgt
im aktuelle Verzeichnis und in ~/texmf/latex/tex/.
Achtung: Sollte das Programm später in einem Temporärem Verzeichnis
arbeiten, nutzt das Paket im aktuellen Verzeichnis nichts!
Achtung: Bei einer Installation wird eine vorhandene Datei
rechnung.sty ohne Nachfrage überschrieben!
returns: boolean
'''
package = "rechnung.sty"
self.D.INFO(f"Teste, ob rechnung.sty installiert ist ... ", end="")
if package in os.listdir():
self.D.TRUE()
return True
for (dirpath, dirnames, filenames) in os.walk(self.texhome):
if package in filenames:
self.D.TRUE()
return True
self.D.FALSE()
return False
def install_sty(self):
'''
Installiert auf Anfrage das Paket rechnung.sty.
'''
url= "https://github.com/tomka/rechnung"
rechnung_sty_dir = "/".join((self.texhome, "tex/latex/rechnung/"))
response = input(" Paket 'rechnung' installieren? (y/N): ")
if response not in ("y", "Y", "j", "J"):
self.D.INFO("Breche das Programm ab")
sys.exit(6)
self.D.INFO("Versuche das Paket 'rechnung' zu installieren.")
self.D.INFO("Nötige Programme: git, latex.")
for program in ("git", "latex"):
if self.check_program(program) is False:
self.D.ERROR(f"Kann {program} nicht finden. Breche ab.")
sys.exit(1)
self.D.INFO("Klone Repo ... ", end="")
response = subprocess.run(["git", "clone", url],
capture_output=True,
timeout=180)
if response.returncode != 0:
self.D.FALSE()
self.D.ERROR(f"Fehler beim Klonen des Repos")
sys.exit(1)
else: self.D.TRUE()
old_pwd = os.environ["PWD"]
os.chdir("rechnung")
self.D.INFO("Versuche 'rechnung.sty' zu bauen ... ", end="")
response = subprocess.run(["latex", "rechnung.ins"], capture_output=True)
if response.returncode != 0:
self.D.FALSE()
sys.exit(1)
else: self.D.TRUE()
self.D.INFO(f"Lege Verzeichnis '{rechnung_sty_dir}' an ... ", end="")
response = subprocess.run(["mkdir", "-p", rechnung_sty_dir])
if response.returncode != 0:
self.D.FALSE()
sys.exit(1)
else: self.D.TRUE()
self.D.INFO(f"Kopiere 'rechnung.sty' nach '{rechnung_sty_dir}' ... ", end="")
response = subprocess.run(["cp", "rechnung.sty", rechnung_sty_dir])
if response.returncode != 0:
self.D.FALSE()
else: self.D.TRUE()
def preup(self):
'''
Führt einige Prüfungen durch.
- Kundendatei und Latex-Template müssen lesbar sein.
- Rechnung.sty muß installiert sein. Ist dies nicht der Fall,
sollte es von Github heruntergeladen und gebaut werden. Dafür
sind die Programme Git und latex nötig.
- Es wird ein Latex-Compiler gebraucht.
'''
for i in (self.rechnungsfile, self.templatefile):
self.check_readable(i)
for program in ('pdflatex', 'xelatex', 'luatex', 'latex'):
if self.check_program(program) == True:
break
if self.check_sty() is False:
self.install_sty()
if self.compiler == "":
self.D.ERROR("Kann keinen Compiler finden. Exit")
sys.exit(2)
def run(self):
'''
'''
self.print_header()
self.preup()
buffer = self.get_file_content(self.rechnungsfile)
if buffer is None:
self.D.ERROR(f"Kann {self.rechnungsfile} nicht lesen. Exit")
sys.exit(3)
self.parse_buffer(buffer)
if __name__ == '__main__':
r = RECHNUNG()
r.run()