467 lines
14 KiB
Python
Executable file
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()
|