#!/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()