README aktualisiert (Port 5001, Settings, Logo, proxmox-update)

This commit is contained in:
Erik Thiele
2026-07-02 16:48:04 +02:00
parent 0522a4c457
commit 980549faa3
2 changed files with 34 additions and 655 deletions

View File

@@ -1,645 +0,0 @@
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from datetime import datetime
from fpdf import FPDF
import os
import yaml
class DruckKalkulation:
def __init__(self, root):
self.root = root
self.root.title("3D-Druck Kostenkalkulation")
self.root.geometry("1200x800")
# Konfigurationsdatei
self.config_file = 'druckkalkulation_config.yaml'
# Dark Mode Status
self.dark_mode = tk.BooleanVar(value=False)
# Theme-Farben
self.themes = {
'light': {
'bg': '#f0f4f8',
'fg': '#1f2937',
'card_bg': 'white',
'card_fg': '#1f2937',
'section_bg': {
'projekt': '#e0e7ff',
'material': '#dbeafe',
'druck': '#d1fae5',
'abschreibung': '#e9d5ff',
'arbeit': '#fed7aa',
'gewinn': '#fef3c7'
},
'result_bg': '#4f46e5',
'result_fg': 'white',
'result_text_bg': '#6366f1',
'detail_bg': '#e5e7eb',
'detail_text_bg': '#f3f4f6',
'entry_bg': 'white',
'entry_fg': '#1f2937'
},
'dark': {
'bg': '#111827',
'fg': '#f9fafb',
'card_bg': '#1f2937',
'card_fg': '#f9fafb',
'section_bg': {
'projekt': '#312e81',
'material': '#1e3a8a',
'druck': '#064e3b',
'abschreibung': '#581c87',
'arbeit': '#92400e',
'gewinn': '#713f12'
},
'result_bg': '#6366f1',
'result_fg': 'white',
'result_text_bg': '#4f46e5',
'detail_bg': '#374151',
'detail_text_bg': '#1f2937',
'entry_bg': '#374151',
'entry_fg': '#f9fafb'
}
}
self.root.configure(bg=self.themes['light']['bg'])
# Variablen
self.vars = {
'projektName': tk.StringVar(),
'kundenName': tk.StringVar(),
'kundenAdresse': tk.StringVar(value=''),
'materialGewicht': tk.DoubleVar(value=0),
'materialPreisProKg': tk.DoubleVar(value=25),
'druckzeit': tk.DoubleVar(value=0),
'stromverbrauch': tk.DoubleVar(value=0.15),
'strompreis': tk.DoubleVar(value=0.35),
'druckerAnschaffung': tk.DoubleVar(value=500),
'druckerNutzungsdauer': tk.DoubleVar(value=3),
'druckerBetriebsstunden': tk.DoubleVar(value=2000),
'arbeitszeit': tk.DoubleVar(value=0),
'stundenlohn': tk.DoubleVar(value=25),
'gewinnmarge': tk.DoubleVar(value=20),
'rechnungsnummer': tk.StringVar(),
'datum': tk.StringVar(value=datetime.now().strftime('%Y-%m-%d'))
}
# Konfiguration laden
self.load_config()
self.create_widgets()
self.update_calculation()
# Automatisches Speichern bei Änderungen
for var in self.vars.values():
var.trace_add('write', lambda *args: self.save_config())
# Dark Mode Status speichern/laden
self.dark_mode.trace_add('write', lambda *args: self.save_config())
def get_current_theme(self):
"""Gibt das aktuelle Theme zurück"""
return self.themes['dark'] if self.dark_mode.get() else self.themes['light']
def toggle_dark_mode(self):
"""Wechselt zwischen Light und Dark Mode"""
self.dark_mode.set(not self.dark_mode.get())
# Button Text aktualisieren
if hasattr(self, 'dark_mode_btn'):
if self.dark_mode.get():
self.dark_mode_btn.config(text="☀️ Light Mode", bg='#1f2937', fg='white')
else:
self.dark_mode_btn.config(text="🌙 Dark Mode", bg='#4f46e5', fg='white')
self.apply_theme()
def apply_theme(self):
"""Wendet das aktuelle Theme auf alle Widgets an"""
theme = self.get_current_theme()
# Root
self.root.configure(bg=theme['bg'])
# Alle Frames durchgehen und Theme anwenden
for widget in self.root.winfo_children():
self.apply_theme_recursive(widget, theme)
self.update_calculation()
def apply_theme_recursive(self, widget, theme):
"""Wendet Theme rekursiv auf alle Widgets an"""
widget_type = widget.winfo_class()
try:
if widget_type in ['Frame', 'Labelframe']:
current_bg = widget.cget('bg')
# Bestimme welche Farbe das Widget haben sollte
if current_bg == 'white' or current_bg.startswith('#f') or current_bg.startswith('#e') or current_bg.startswith('#d'):
if hasattr(self, 'title_frame') and widget == self.title_frame:
widget.configure(bg=theme['card_bg'])
elif current_bg in ['#e0e7ff', '#312e81']:
widget.configure(bg=theme['section_bg']['projekt'])
elif current_bg in ['#dbeafe', '#1e3a8a']:
widget.configure(bg=theme['section_bg']['material'])
elif current_bg in ['#d1fae5', '#064e3b']:
widget.configure(bg=theme['section_bg']['druck'])
elif current_bg in ['#e9d5ff', '#581c87']:
widget.configure(bg=theme['section_bg']['abschreibung'])
elif current_bg in ['#fed7aa', '#92400e']:
widget.configure(bg=theme['section_bg']['arbeit'])
elif current_bg in ['#fef3c7', '#713f12']:
widget.configure(bg=theme['section_bg']['gewinn'])
elif current_bg in ['#e5e7eb', '#374151']:
widget.configure(bg=theme['detail_bg'])
else:
widget.configure(bg=theme['bg'])
elif current_bg in ['#4f46e5', '#6366f1']:
widget.configure(bg=theme['result_bg'])
elif current_bg in ['#111827', '#f0f4f8']:
widget.configure(bg=theme['bg'])
elif widget_type == 'Label':
current_bg = widget.cget('bg')
if current_bg in ['#4f46e5', '#6366f1']:
widget.configure(bg=theme['result_bg'], fg=theme['result_fg'])
elif current_bg == 'white' or current_bg.startswith('#f') and widget != self.title_frame:
widget.configure(bg=theme['card_bg'], fg=theme['card_fg'])
elif current_bg in ['#e0e7ff', '#312e81']:
widget.configure(bg=theme['section_bg']['projekt'], fg=theme['fg'])
elif current_bg in ['#dbeafe', '#1e3a8a']:
widget.configure(bg=theme['section_bg']['material'], fg=theme['fg'])
elif current_bg in ['#d1fae5', '#064e3b']:
widget.configure(bg=theme['section_bg']['druck'], fg=theme['fg'])
elif current_bg in ['#e9d5ff', '#581c87']:
widget.configure(bg=theme['section_bg']['abschreibung'], fg=theme['fg'])
elif current_bg in ['#fed7aa', '#92400e']:
widget.configure(bg=theme['section_bg']['arbeit'], fg=theme['fg'])
elif current_bg in ['#fef3c7', '#713f12']:
widget.configure(bg=theme['section_bg']['gewinn'], fg=theme['fg'])
elif current_bg in ['#e5e7eb', '#374151']:
widget.configure(bg=theme['detail_bg'], fg=theme['fg'])
else:
widget.configure(bg=theme['bg'], fg=theme['fg'])
elif widget_type == 'Entry':
widget.configure(bg=theme['entry_bg'], fg=theme['entry_fg'],
insertbackground=theme['entry_fg'])
elif widget_type == 'Text':
if hasattr(self, 'result_text') and widget == self.result_text:
widget.configure(bg=theme['result_text_bg'], fg=theme['result_fg'])
elif hasattr(self, 'detail_text') and widget == self.detail_text:
widget.configure(bg=theme['detail_text_bg'], fg=theme['fg'])
else:
widget.configure(bg=theme['entry_bg'], fg=theme['entry_fg'])
elif widget_type == 'Canvas':
widget.configure(bg=theme['bg'])
except tk.TclError:
pass
# Rekursiv für alle Kinder
for child in widget.winfo_children():
self.apply_theme_recursive(child, theme)
def create_widgets(self):
# Hauptcontainer
main_frame = tk.Frame(self.root, bg=self.get_current_theme()['bg'])
main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
# Titel mit Dark Mode Toggle
self.title_frame = tk.Frame(main_frame, bg=self.get_current_theme()['card_bg'], relief=tk.RAISED, bd=2)
self.title_frame.pack(fill=tk.X, pady=(0, 20))
title_content = tk.Frame(self.title_frame, bg=self.get_current_theme()['card_bg'])
title_content.pack(fill=tk.X, padx=15, pady=15)
tk.Label(title_content, text="🖨️ 3D-Druck Kostenkalkulation",
font=('Arial', 24, 'bold'), bg=self.get_current_theme()['card_bg'],
fg='#4f46e5').pack(side=tk.LEFT)
# Dark Mode Toggle Button
dark_mode_frame = tk.Frame(title_content, bg=self.get_current_theme()['card_bg'])
dark_mode_frame.pack(side=tk.RIGHT, padx=10)
self.dark_mode_btn = tk.Button(
dark_mode_frame,
text="☀️ Light Mode" if self.dark_mode.get() else "🌙 Dark Mode",
command=self.toggle_dark_mode,
font=('Arial', 11, 'bold'),
bg='#4f46e5',
fg='white',
padx=15,
pady=8,
relief=tk.RAISED,
cursor='hand2',
borderwidth=2
)
self.dark_mode_btn.pack()
# Container für linke und rechte Spalte
content_frame = tk.Frame(main_frame, bg=self.get_current_theme()['bg'])
content_frame.pack(fill=tk.BOTH, expand=True)
# Linke Spalte - Eingaben
left_frame = tk.Frame(content_frame, bg=self.get_current_theme()['bg'])
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
# Scrollbarer Bereich für Eingaben
canvas = tk.Canvas(left_frame, bg=self.get_current_theme()['bg'], highlightthickness=0)
scrollbar = ttk.Scrollbar(left_frame, orient="vertical", command=canvas.yview)
scrollable_frame = tk.Frame(canvas, bg=self.get_current_theme()['bg'])
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Projektinformationen
self.create_section(scrollable_frame, "Projektinformationen", 'projekt', [
('Projektname', 'projektName', 'entry')
])
# Material
self.create_section(scrollable_frame, "Material", 'material', [
('Gewicht (Gramm)', 'materialGewicht', 'number'),
('Preis pro kg (€)', 'materialPreisProKg', 'number')
])
# Druckparameter
self.create_section(scrollable_frame, "Druckparameter", 'druck', [
('Druckzeit (Minuten)', 'druckzeit', 'number'),
('Stromverbrauch (kW)', 'stromverbrauch', 'number'),
('Strompreis (€/kWh)', 'strompreis', 'number')
])
# Drucker Abschreibung
self.create_section(scrollable_frame, "Drucker Abschreibung", 'abschreibung', [
('Anschaffungspreis (€)', 'druckerAnschaffung', 'number'),
('Nutzungsdauer (Jahre)', 'druckerNutzungsdauer', 'number'),
('Betriebsstunden pro Jahr', 'druckerBetriebsstunden', 'number')
])
# Arbeitskosten
self.create_section(scrollable_frame, "Arbeitskosten", 'arbeit', [
('Arbeitszeit (Minuten)', 'arbeitszeit', 'number'),
('Stundenlohn (€)', 'stundenlohn', 'number')
])
# Gewinnmarge
self.create_section(scrollable_frame, "Gewinnmarge", 'gewinn', [
('Gewinnmarge (%)', 'gewinnmarge', 'number')
])
# Rechte Spalte - Ergebnisse
right_frame = tk.Frame(content_frame, bg=self.get_current_theme()['bg'])
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0))
# Ergebnisbereich
self.result_frame = tk.Frame(right_frame, bg=self.get_current_theme()['result_bg'], relief=tk.RAISED, bd=3)
self.result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
tk.Label(self.result_frame, text="Kostenaufstellung",
font=('Arial', 18, 'bold'), bg=self.get_current_theme()['result_bg'],
fg=self.get_current_theme()['result_fg']).pack(pady=15)
self.result_text = tk.Text(self.result_frame, height=20, width=40,
font=('Courier', 11), bg=self.get_current_theme()['result_text_bg'],
fg=self.get_current_theme()['result_fg'],
relief=tk.FLAT, padx=15, pady=10)
self.result_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
# Detaillierte Informationen
detail_frame = tk.Frame(right_frame, bg=self.get_current_theme()['detail_bg'], relief=tk.RAISED, bd=2)
detail_frame.pack(fill=tk.X, pady=(0, 10))
tk.Label(detail_frame, text="Detaillierte Informationen",
font=('Arial', 12, 'bold'), bg=self.get_current_theme()['detail_bg'],
fg=self.get_current_theme()['fg']).pack(pady=10)
self.detail_text = tk.Text(detail_frame, height=5, width=40,
font=('Arial', 10), bg=self.get_current_theme()['detail_text_bg'],
fg=self.get_current_theme()['fg'],
relief=tk.FLAT, padx=10, pady=5)
self.detail_text.pack(fill=tk.X, padx=10, pady=(0, 10))
# Buttons
button_frame = tk.Frame(right_frame, bg='#f0f4f8')
button_frame.pack(fill=tk.X)
tk.Button(button_frame, text="📄 Rechnung erstellen",
command=self.create_invoice, bg='#4f46e5', fg='white',
font=('Arial', 12, 'bold'), padx=20, pady=10,
cursor='hand2').pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
tk.Button(button_frame, text="💾 Export",
command=self.export_data, bg='#10b981', fg='white',
font=('Arial', 12, 'bold'), padx=20, pady=10,
cursor='hand2').pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=(5, 0))
def save_config(self):
"""Speichert alle Parameter in YAML-Datei"""
try:
config_data = {}
for key, var in self.vars.items():
value = var.get()
config_data[key] = value
# Dark Mode Status speichern
config_data['dark_mode'] = self.dark_mode.get()
with open(self.config_file, 'w', encoding='utf-8') as f:
yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True)
except Exception as e:
print(f"Fehler beim Speichern der Konfiguration: {e}")
def load_config(self):
"""Lädt alle Parameter aus YAML-Datei"""
try:
if os.path.exists(self.config_file):
with open(self.config_file, 'r', encoding='utf-8') as f:
config_data = yaml.safe_load(f)
if config_data:
for key, value in config_data.items():
if key == 'dark_mode':
self.dark_mode.set(value)
elif key in self.vars:
self.vars[key].set(value)
print(f"✓ Konfiguration geladen aus: {self.config_file}")
else:
print(f" Keine Konfigurationsdatei gefunden. Standard-Werte werden verwendet.")
except Exception as e:
print(f"Fehler beim Laden der Konfiguration: {e}")
messagebox.showwarning("Warnung",
f"Konfiguration konnte nicht geladen werden.\nStandard-Werte werden verwendet.")
def create_section(self, parent, title, theme_key, fields):
theme = self.get_current_theme()
bg_color = theme['section_bg'][theme_key]
frame = tk.Frame(parent, bg=bg_color, relief=tk.RAISED, bd=2)
frame.pack(fill=tk.X, pady=(0, 10))
tk.Label(frame, text=title, font=('Arial', 12, 'bold'),
bg=bg_color, fg=theme['fg']).pack(anchor='w', padx=10, pady=(10, 5))
for label, var_name, field_type in fields:
field_frame = tk.Frame(frame, bg=bg_color)
field_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label(field_frame, text=label, bg=bg_color, fg=theme['fg'],
font=('Arial', 10)).pack(anchor='w')
if field_type == 'entry':
entry = tk.Entry(field_frame, textvariable=self.vars[var_name],
font=('Arial', 10), bg=theme['entry_bg'],
fg=theme['entry_fg'], insertbackground=theme['entry_fg'])
else:
entry = tk.Entry(field_frame, textvariable=self.vars[var_name],
font=('Arial', 10), bg=theme['entry_bg'],
fg=theme['entry_fg'], insertbackground=theme['entry_fg'])
entry.pack(fill=tk.X, pady=(2, 0))
entry.bind('<KeyRelease>', lambda e: self.update_calculation())
def get_calculations(self):
try:
# Umrechnung
druckzeit_stunden = self.vars['druckzeit'].get() / 60
arbeitszeit_stunden = self.vars['arbeitszeit'].get() / 60
material_kg = self.vars['materialGewicht'].get() / 1000
# Berechnungen
materialkosten = material_kg * self.vars['materialPreisProKg'].get()
stromkosten = druckzeit_stunden * self.vars['stromverbrauch'].get() * self.vars['strompreis'].get()
abschreibung_pro_stunde = self.vars['druckerAnschaffung'].get() / (
self.vars['druckerNutzungsdauer'].get() * self.vars['druckerBetriebsstunden'].get()
)
abschreibung = abschreibung_pro_stunde * druckzeit_stunden
arbeitskosten = arbeitszeit_stunden * self.vars['stundenlohn'].get()
gesamtkosten_netto = materialkosten + stromkosten + abschreibung + arbeitskosten
gewinn = gesamtkosten_netto * (self.vars['gewinnmarge'].get() / 100)
verkaufspreis = gesamtkosten_netto + gewinn
mwst = verkaufspreis * 0.19
gesamt_brutto = verkaufspreis + mwst
return {
'materialkosten': materialkosten,
'stromkosten': stromkosten,
'abschreibung': abschreibung,
'arbeitskosten': arbeitskosten,
'gesamtkosten_netto': gesamtkosten_netto,
'gewinn': gewinn,
'verkaufspreis': verkaufspreis,
'mwst': mwst,
'gesamt_brutto': gesamt_brutto,
'druckzeit_stunden': druckzeit_stunden,
'material_kg': material_kg,
'abschreibung_pro_stunde': abschreibung_pro_stunde
}
except:
return None
def update_calculation(self):
calc = self.get_calculations()
if not calc:
return
# Ergebnis-Text
self.result_text.delete(1.0, tk.END)
result_str = f"""
Materialkosten: {calc['materialkosten']:>10.2f}
Stromkosten: {calc['stromkosten']:>10.2f}
Abschreibung Drucker: {calc['abschreibung']:>10.2f}
Arbeitskosten: {calc['arbeitskosten']:>10.2f}
{'' * 40}
Gesamtkosten: {calc['gesamtkosten_netto']:>10.2f}
Gewinn ({self.vars['gewinnmarge'].get():.0f}%): {calc['gewinn']:>10.2f}
{'' * 40}
Verkaufspreis (netto): {calc['verkaufspreis']:>10.2f}
MwSt. (19%): {calc['mwst']:>10.2f}
{'' * 40}
GESAMT (brutto): {calc['gesamt_brutto']:>10.2f}
"""
self.result_text.insert(1.0, result_str)
# Detail-Text
self.detail_text.delete(1.0, tk.END)
detail_str = f"""Druckzeit: {calc['druckzeit_stunden']:.2f} Stunden
Materialgewicht: {calc['material_kg']:.3f} kg
Abschreibung pro Stunde: {calc['abschreibung_pro_stunde']:.4f}
Kosten pro Gramm: {self.vars['materialPreisProKg'].get()/1000:.3f}
Stromkosten pro Stunde: {self.vars['stromverbrauch'].get() * self.vars['strompreis'].get():.3f}"""
self.detail_text.insert(1.0, detail_str)
def export_data(self):
calc = self.get_calculations()
if not calc:
messagebox.showerror("Fehler", "Bitte füllen Sie alle Felder aus")
return
filename = filedialog.asksaveasfilename(
defaultextension=".txt",
filetypes=[("Text Datei", "*.txt"), ("Alle Dateien", "*.*")]
)
if filename:
with open(filename, 'w', encoding='utf-8') as f:
f.write("3D-DRUCK KOSTENKALKULATION\n")
f.write("=" * 50 + "\n\n")
f.write(f"Projekt: {self.vars['projektName'].get()}\n")
f.write(f"Datum: {self.vars['datum'].get()}\n\n")
f.write("KOSTENAUFSTELLUNG:\n")
f.write(f"Materialkosten: {calc['materialkosten']:.2f}\n")
f.write(f"Stromkosten: {calc['stromkosten']:.2f}\n")
f.write(f"Abschreibung: {calc['abschreibung']:.2f}\n")
f.write(f"Arbeitskosten: {calc['arbeitskosten']:.2f}\n")
f.write(f"Gesamtkosten: {calc['gesamtkosten_netto']:.2f}\n")
f.write(f"Gewinn: {calc['gewinn']:.2f}\n")
f.write(f"Verkaufspreis (netto): {calc['verkaufspreis']:.2f}\n")
f.write(f"MwSt. (19%): {calc['mwst']:.2f}\n")
f.write(f"GESAMTPREIS (brutto): {calc['gesamt_brutto']:.2f}\n")
messagebox.showinfo("Erfolg", "Daten erfolgreich exportiert!")
def create_invoice(self):
# Rechnungsfenster
invoice_window = tk.Toplevel(self.root)
invoice_window.title("Rechnung erstellen")
invoice_window.geometry("600x700")
invoice_window.configure(bg='white')
frame = tk.Frame(invoice_window, bg='white', padx=20, pady=20)
frame.pack(fill=tk.BOTH, expand=True)
tk.Label(frame, text="RECHNUNG", font=('Arial', 24, 'bold'),
bg='white').pack(pady=(0, 20))
# Kundendaten
tk.Label(frame, text="Rechnungsnummer:", bg='white',
font=('Arial', 10)).pack(anchor='w')
tk.Entry(frame, textvariable=self.vars['rechnungsnummer'],
font=('Arial', 10)).pack(fill=tk.X, pady=(0, 10))
tk.Label(frame, text="Datum:", bg='white', font=('Arial', 10)).pack(anchor='w')
tk.Entry(frame, textvariable=self.vars['datum'],
font=('Arial', 10)).pack(fill=tk.X, pady=(0, 10))
tk.Label(frame, text="Kundenname:", bg='white', font=('Arial', 10)).pack(anchor='w')
tk.Entry(frame, textvariable=self.vars['kundenName'],
font=('Arial', 10)).pack(fill=tk.X, pady=(0, 10))
tk.Label(frame, text="Kundenadresse:", bg='white', font=('Arial', 10)).pack(anchor='w')
address_text = tk.Text(frame, height=3, font=('Arial', 10))
address_text.pack(fill=tk.X, pady=(0, 20))
address_text.insert(1.0, self.vars['kundenAdresse'].get())
calc = self.get_calculations()
if calc:
# Rechnungsvorschau
preview = tk.Text(frame, height=15, font=('Courier', 10), bg='#f9fafb')
preview.pack(fill=tk.BOTH, expand=True, pady=(0, 20))
preview_text = f"""
LEISTUNGSBESCHREIBUNG:
{'' * 50}
Position: 3D-Druck - {self.vars['projektName'].get()}
Menge: 1
Einzelpreis: {calc['verkaufspreis']:.2f}
Gesamt: {calc['verkaufspreis']:.2f}
{'' * 50}
Nettobetrag: {calc['verkaufspreis']:.2f}
MwSt. (19%): {calc['mwst']:.2f}
{'' * 50}
GESAMTBETRAG: {calc['gesamt_brutto']:.2f}
Zahlbar innerhalb von 14 Tagen ohne Abzug.
Vielen Dank für Ihren Auftrag!
"""
preview.insert(1.0, preview_text)
preview.config(state='disabled')
tk.Button(frame, text="💾 Rechnung als PDF speichern",
command=lambda: self.save_pdf_invoice(address_text.get(1.0, tk.END)),
bg='#1f2937', fg='white', font=('Arial', 12, 'bold'),
padx=20, pady=10, cursor='hand2').pack(fill=tk.X)
def save_pdf_invoice(self, address):
try:
calc = self.get_calculations()
if not calc:
messagebox.showerror("Fehler", "Berechnung fehlgeschlagen")
return
filename = filedialog.asksaveasfilename(
defaultextension=".pdf",
filetypes=[("PDF Datei", "*.pdf")]
)
if not filename:
return
pdf = FPDF()
pdf.add_page()
pdf.set_font("Arial", "B", 24)
pdf.cell(0, 15, "RECHNUNG", ln=True, align='C')
pdf.ln(10)
pdf.set_font("Arial", "", 11)
pdf.cell(0, 6, f"Rechnungsnummer: {self.vars['rechnungsnummer'].get()}", ln=True)
pdf.cell(0, 6, f"Datum: {self.vars['datum'].get()}", ln=True)
pdf.ln(10)
pdf.set_font("Arial", "B", 12)
pdf.cell(0, 6, "Rechnungsempfänger:", ln=True)
pdf.set_font("Arial", "", 11)
pdf.cell(0, 6, self.vars['kundenName'].get(), ln=True)
pdf.multi_cell(0, 6, address)
pdf.ln(5)
pdf.set_font("Arial", "B", 12)
pdf.cell(0, 8, "Leistungsbeschreibung:", ln=True)
pdf.set_font("Arial", "", 11)
pdf.cell(100, 8, f"3D-Druck: {self.vars['projektName'].get()}")
pdf.cell(30, 8, "1", align='R')
pdf.cell(30, 8, f"{calc['verkaufspreis']:.2f} EUR", align='R')
pdf.cell(30, 8, f"{calc['verkaufspreis']:.2f} EUR", ln=True, align='R')
pdf.ln(10)
pdf.cell(130, 6, "Nettobetrag:")
pdf.cell(60, 6, f"{calc['verkaufspreis']:.2f} EUR", ln=True, align='R')
pdf.cell(130, 6, "MwSt. (19%):")
pdf.cell(60, 6, f"{calc['mwst']:.2f} EUR", ln=True, align='R')
pdf.set_font("Arial", "B", 12)
pdf.cell(130, 8, "GESAMTBETRAG:")
pdf.cell(60, 8, f"{calc['gesamt_brutto']:.2f} EUR", ln=True, align='R')
pdf.ln(10)
pdf.set_font("Arial", "", 10)
pdf.multi_cell(0, 5, "Zahlbar innerhalb von 14 Tagen ohne Abzug.\nVielen Dank für Ihren Auftrag!")
pdf.output(filename)
messagebox.showinfo("Erfolg", "PDF-Rechnung erfolgreich erstellt!")
except Exception as e:
messagebox.showerror("Fehler", f"PDF konnte nicht erstellt werden.\nBitte installieren Sie: pip install fpdf\n\nFehler: {str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = DruckKalkulation(root)
root.mainloop()

View File

@@ -15,15 +15,17 @@ pip install flask fpdf2 pyyaml
python app.py
```
-> http://127.0.0.1:5000
-> http://127.0.0.1:5001
## Funktionen
- **Live-Berechnung** bei jeder Eingabe (Material, Strom, Abschreibung, Arbeit, Gewinn)
- **Dark Mode** (persistiert in YAML-Konfiguration)
- **PDF-Rechnung** mit Kundendaten (via fpdf2)
- **PDF-Rechnung** mit Logo, Kundendaten und Positionstabelle (via fpdf2)
- **TXT-Export** der Kalkulation
- **Auto-Save** aller Werte via AJAX (debounced 500ms)
- **Einstellungsseite** für Druckerabschreibung, Arbeitskosten, MwSt., Stromkosten, Firmendaten, Logo
- **Logo-Upload** (PNG/JPEG/WebP) für die Rechnung
## Installation auf Proxmox (LXC-Container)
@@ -37,7 +39,7 @@ systemd-Service ein.
bash -c "$(curl -fsSL https://gitea.teamthiele.de/ethiele/3ddruckkalkulation/raw/branch/main/proxmox-setup.sh)"
```
Nach Abschluss ist die App unter `http://<CONTAINER-IP>:5000` erreichbar.
Nach Abschluss ist die App unter `http://<CONTAINER-IP>:5001` erreichbar.
Container-ID und Name konnen als Argumente ubergeben werden:
```bash
@@ -52,6 +54,25 @@ systemctl status 3ddruck-kalkulation.service
journalctl -u 3ddruck-kalkulation.service -f
```
## Update des Containers
`proxmox-update.sh` aktualisiert den Container via Git und startet den Service neu.
**Im Container ausfuhren:**
```bash
./proxmox-update.sh
```
**Vom Proxmox-Host aus:**
```bash
./proxmox-update.sh 201
```
Das Skript holt via `curl` die aktuelle Version aus dem Gitea-Repo, pullt,
installiert ggf. neue Dependencies und restartet den systemd-Service.
## Technik
| Komponente | Technologie |
@@ -68,8 +89,11 @@ journalctl -u 3ddruck-kalkulation.service -f
app.py # Flask-Server mit allen Routen
templates/index.html # Hauptseite (Formular + Ergebnisse)
templates/invoice.html # Rechnungsseite
templates/settings.html # Einstellungen
static/style.css # Minimales Custom-CSS
static/script.js # Live-Berechnung + Auto-Save
proxmox-setup.sh # Ersteinrichtung LXC-Container
proxmox-update.sh # Update LXC-Container
druckkalkulation_config.yaml # Automatisch erstellte Konfiguration
3dDruckKalkulator.py # Alte Tkinter-Version (unused)
```