From 980549faa32eaa1100d2da659d5fd392a50922c1 Mon Sep 17 00:00:00 2001 From: Erik Thiele Date: Thu, 2 Jul 2026 16:48:04 +0200 Subject: [PATCH] README aktualisiert (Port 5001, Settings, Logo, proxmox-update) --- 3dDruckKalkulator.py | 645 ------------------------------------------- README.md | 44 ++- 2 files changed, 34 insertions(+), 655 deletions(-) delete mode 100644 3dDruckKalkulator.py diff --git a/3dDruckKalkulator.py b/3dDruckKalkulator.py deleted file mode 100644 index a7cd27d..0000000 --- a/3dDruckKalkulator.py +++ /dev/null @@ -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( - "", - 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('', 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() \ No newline at end of file diff --git a/README.md b/README.md index 246eb3c..e674daa 100644 --- a/README.md +++ b/README.md @@ -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://:5000` erreichbar. +Nach Abschluss ist die App unter `http://: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 | @@ -65,13 +86,16 @@ journalctl -u 3ddruck-kalkulation.service -f ## Projektstruktur ``` -app.py # Flask-Server mit allen Routen -templates/index.html # Hauptseite (Formular + Ergebnisse) -templates/invoice.html # Rechnungsseite -static/style.css # Minimales Custom-CSS -static/script.js # Live-Berechnung + Auto-Save -druckkalkulation_config.yaml # Automatisch erstellte Konfiguration -3dDruckKalkulator.py # Alte Tkinter-Version (unused) +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) ``` ## Hinweise