README aktualisiert (Port 5001, Settings, Logo, proxmox-update)
This commit is contained in:
@@ -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()
|
|
||||||
44
README.md
44
README.md
@@ -15,15 +15,17 @@ pip install flask fpdf2 pyyaml
|
|||||||
python app.py
|
python app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
-> http://127.0.0.1:5000
|
-> http://127.0.0.1:5001
|
||||||
|
|
||||||
## Funktionen
|
## Funktionen
|
||||||
|
|
||||||
- **Live-Berechnung** bei jeder Eingabe (Material, Strom, Abschreibung, Arbeit, Gewinn)
|
- **Live-Berechnung** bei jeder Eingabe (Material, Strom, Abschreibung, Arbeit, Gewinn)
|
||||||
- **Dark Mode** (persistiert in YAML-Konfiguration)
|
- **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
|
- **TXT-Export** der Kalkulation
|
||||||
- **Auto-Save** aller Werte via AJAX (debounced 500ms)
|
- **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)
|
## 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)"
|
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:
|
Container-ID und Name konnen als Argumente ubergeben werden:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -52,6 +54,25 @@ systemctl status 3ddruck-kalkulation.service
|
|||||||
journalctl -u 3ddruck-kalkulation.service -f
|
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
|
## Technik
|
||||||
|
|
||||||
| Komponente | Technologie |
|
| Komponente | Technologie |
|
||||||
@@ -65,13 +86,16 @@ journalctl -u 3ddruck-kalkulation.service -f
|
|||||||
## Projektstruktur
|
## Projektstruktur
|
||||||
|
|
||||||
```
|
```
|
||||||
app.py # Flask-Server mit allen Routen
|
app.py # Flask-Server mit allen Routen
|
||||||
templates/index.html # Hauptseite (Formular + Ergebnisse)
|
templates/index.html # Hauptseite (Formular + Ergebnisse)
|
||||||
templates/invoice.html # Rechnungsseite
|
templates/invoice.html # Rechnungsseite
|
||||||
static/style.css # Minimales Custom-CSS
|
templates/settings.html # Einstellungen
|
||||||
static/script.js # Live-Berechnung + Auto-Save
|
static/style.css # Minimales Custom-CSS
|
||||||
druckkalkulation_config.yaml # Automatisch erstellte Konfiguration
|
static/script.js # Live-Berechnung + Auto-Save
|
||||||
3dDruckKalkulator.py # Alte Tkinter-Version (unused)
|
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
|
## Hinweise
|
||||||
|
|||||||
Reference in New Issue
Block a user