Files
3ddruckkalkulation/app.py

384 lines
13 KiB
Python

"""
3D-Druck Kostenkalkulation — Flask Web-App.
Ersetzt die alte Tkinter-Version (3dDruckKalkulator.py).
Bietet eine Weboberflaeche zur Eingabe von Druckparametern,
Live-Berechnung der Kosten, PDF-Rechnung und TXT-Export.
"""
import os
import yaml
import io
from datetime import datetime
from flask import Flask, render_template, request, jsonify, redirect, url_for, send_file
from fpdf import FPDF
app = Flask(__name__)
CONFIG_FILE = 'druckkalkulation_config.yaml'
DEFAULT_VALUES = {
'projektName': '',
'kundenName': '',
'kundenAdresse': '',
'materialGewicht': 0,
'materialPreisProKg': 25,
'druckzeit': 0,
'stromverbrauch': 0.15,
'strompreis': 0.35,
'druckerAnschaffung': 500,
'druckerNutzungsdauer': 3,
'druckerBetriebsstunden': 2000,
'arbeitszeit': 0,
'stundenlohn': 25,
'gewinnmarge': 20,
'stueckzahl': 1,
'mwst_ausweisen': False,
'rechnungsnummer': '',
'datum': datetime.now().strftime('%Y-%m-%d'),
'dark_mode': False,
'firmenName': '',
'firmenAdresse': '',
'telefon': '',
'email': '',
'paypal': '',
'iban': '',
'bankName': '',
}
def load_config():
"""Liest die YAML-Konfiguration von Disk. Fehlt die Datei,
werden DEFAULT_VALUES verwendet."""
try:
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
if data:
for k in DEFAULT_VALUES:
if k not in data:
data[k] = DEFAULT_VALUES[k]
return data
except Exception as e:
print(f"Fehler beim Laden: {e}")
return dict(DEFAULT_VALUES)
def save_config(data):
"""Schreibt das uebergebene Dict als YAML-Datei."""
try:
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
except Exception as e:
print(f"Fehler beim Speichern: {e}")
def calculate(data):
"""Berechnet alle Kosten aus den Formularwerten.
Rueckgabe: Dict mit Einzelposten, Gesamtsummen und Detailwerten.
"""
# Umrechnungen
druckzeit_stunden = float(data.get('druckzeit', 0)) / 60
arbeitszeit_stunden = float(data.get('arbeitszeit', 0)) / 60
material_kg = float(data.get('materialGewicht', 0)) / 1000
# Material-, Strom-, Abschreibungs- und Arbeitskosten
materialkosten = material_kg * float(data.get('materialPreisProKg', 0))
stromkosten = druckzeit_stunden * float(data.get('stromverbrauch', 0)) * float(data.get('strompreis', 0))
anschaffung = float(data.get('druckerAnschaffung', 0))
nutzung = float(data.get('druckerNutzungsdauer', 1))
betrieb = float(data.get('druckerBetriebsstunden', 1))
abschreibung_pro_stunde = anschaffung / (nutzung * betrieb) if (nutzung * betrieb) > 0 else 0
abschreibung = abschreibung_pro_stunde * druckzeit_stunden
arbeitskosten = arbeitszeit_stunden * float(data.get('stundenlohn', 0))
# Gewinn, MwSt. und Bruttopreis
gesamtkosten_netto = materialkosten + stromkosten + abschreibung + arbeitskosten
gewinn = gesamtkosten_netto * (float(data.get('gewinnmarge', 0)) / 100)
verkaufspreis = gesamtkosten_netto + gewinn
mwst_ausweisen = str(data.get('mwst_ausweisen', '')).lower() in ('true', '1', 'yes')
mwst = verkaufspreis * 0.19 if mwst_ausweisen else 0
gesamt_brutto = verkaufspreis + mwst
material_preis_pro_kg = float(data.get('materialPreisProKg', 0))
return {
'materialkosten': round(materialkosten, 2),
'stromkosten': round(stromkosten, 2),
'abschreibung': round(abschreibung, 2),
'arbeitskosten': round(arbeitskosten, 2),
'gesamtkosten_netto': round(gesamtkosten_netto, 2),
'gewinn': round(gewinn, 2),
'verkaufspreis': round(verkaufspreis, 2),
'mwst': round(mwst, 2),
'mwst_ausweisen': mwst_ausweisen,
'gesamt_brutto': round(gesamt_brutto, 2),
'druckzeit_stunden': round(druckzeit_stunden, 2),
'material_kg': round(material_kg, 3),
'abschreibung_pro_stunde': round(abschreibung_pro_stunde, 4),
'kosten_pro_gramm': round(material_preis_pro_kg / 1000, 3),
'strom_pro_stunde': round(float(data.get('stromverbrauch', 0)) * float(data.get('strompreis', 0)), 3),
}
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@app.route('/')
def index():
"""Hauptseite mit Eingabeformular und Ergebnissen."""
config = load_config()
calc = calculate(config)
return render_template('index.html', config=config, calc=calc)
@app.route('/settings')
def settings():
"""Einstellungsseite fuer Druckerabschreibung, Arbeitskosten und Marge."""
config = load_config()
return render_template('settings.html', config=config)
@app.route('/calculate', methods=['POST'])
def calc_json():
"""AJAX-Endpunkt: Nimmt Formular-JSON entgegen, gibt Berechnung zurueck.
Fehlende Werte werden aus der gespeicherten Config ergaenzt."""
data = request.get_json()
config = load_config()
merged = {**config, **data}
calc = calculate(merged)
return jsonify(calc)
@app.route('/save_config', methods=['POST'])
def save_config_route():
"""AJAX-Endpunkt: Speichert aktuelle Formularwerte in YAML.
Vorhandene Werte (z.B. aus Einstellungen) werden nicht ueberschrieben."""
data = request.get_json()
existing = load_config()
existing.update(data)
save_config(existing)
return jsonify({'status': 'ok'})
@app.route('/invoice')
def invoice():
"""Rechnungsseite mit Kundendaten und Vorschau."""
config = load_config()
calc = calculate(config)
return render_template('invoice.html', config=config, calc=calc)
@app.route('/invoice/pdf', methods=['POST'])
def invoice_pdf():
"""Generiert eine PDF-Rechnung via fpdf2 und sendet sie zum Download."""
data = request.form
config = load_config()
calc = calculate(config)
stueckzahl = int(config.get('stueckzahl', 1))
gesamt_netto = round(calc['verkaufspreis'] * stueckzahl, 2)
gesamt_mwst = round(gesamt_netto * 0.19, 2) if calc.get('mwst_ausweisen') else 0
gesamt_brutto = round(gesamt_netto + gesamt_mwst, 2)
firmen_name = config.get('firmenName', '')
firmen_adresse = config.get('firmenAdresse', '')
telefon = config.get('telefon', '')
email = config.get('email', '')
paypal = config.get('paypal', '')
iban = config.get('iban', '')
bank_name = config.get('bankName', '')
kunde = data.get('kundenName', config.get('kundenName', ''))
adresse = data.get('kundenAdresse', config.get('kundenAdresse', ''))
rechnungsnummer = data.get('rechnungsnummer', config.get('rechnungsnummer', ''))
datum = data.get('datum', config.get('datum', ''))
projekt = config.get('projektName', '')
pdf = FPDF()
pdf.add_page()
left = 15
right_margin = 15
col_left = left
col_right = 120
col_w = 80
# --- Header: Absender links, Logo rechts ---
pdf.set_font("Arial", "B", 11)
pdf.set_xy(col_left, 15)
pdf.cell(col_w, 5, firmen_name, ln=True)
pdf.set_font("Arial", "", 9)
if firmen_adresse:
pdf.set_x(col_left)
pdf.cell(col_w, 4, firmen_adresse, ln=True)
contact_parts = []
if telefon:
contact_parts.append(f"Tel: {telefon}")
if email:
contact_parts.append(f"Mail: {email}")
if contact_parts:
pdf.set_x(col_left)
pdf.cell(col_w, 4, " | ".join(contact_parts), ln=True)
# Logo rechts
pdf.set_font("Arial", "B", 22)
pdf.set_xy(col_right, 15)
y_logo = pdf.get_y()
line1 = "JET"
line2 = "Design"
pdf.set_xy(col_right, y_logo)
pdf.set_text_color(6, 111, 209)
pdf.cell(col_w, 8, line1, ln=True, align='R')
pdf.set_x(col_right)
pdf.cell(col_w, 8, line2, ln=True, align='R')
pdf.set_text_color(0, 0, 0)
# --- Rechnungsdaten ---
invoice_y = max(pdf.get_y(), 50)
pdf.set_xy(left, invoice_y)
pdf.set_font("Arial", "", 10)
if rechnungsnummer:
pdf.cell(col_w, 5, f"Rechnungsnr.: {rechnungsnummer}", ln=True)
pdf.set_x(left)
pdf.cell(col_w, 5, f"Datum: {datum}", ln=True)
# --- Empfänger ---
pdf.set_xy(left, pdf.get_y() + 10)
pdf.set_font("Arial", "B", 10)
pdf.cell(col_w, 5, "Rechnungsempfanger:", ln=True)
pdf.set_font("Arial", "", 10)
pdf.set_x(left)
pdf.cell(col_w, 5, kunde, ln=True)
if adresse:
pdf.set_x(left)
pdf.set_font("Arial", "", 9)
for line in adresse.split('\n'):
pdf.set_x(left)
pdf.cell(col_w, 4, line.strip(), ln=True)
# --- Trennlinie ---
pdf.set_draw_color(220, 220, 220)
pdf.line(left, pdf.get_y() + 6, 195, pdf.get_y() + 6)
pdf.set_draw_color(0, 0, 0)
# --- Positionstabelle ---
table_y = pdf.get_y() + 10
pdf.set_xy(left, table_y)
pdf.set_font("Arial", "B", 10)
pdf.cell(85, 6, "Leistungsbeschreibung")
pdf.cell(20, 6, "Menge", align='R')
pdf.cell(30, 6, "Einzelpreis", align='R')
pdf.cell(30, 6, "Gesamt", ln=True, align='R')
pdf.set_draw_color(200, 200, 200)
pdf.line(left, pdf.get_y() + 0.5, 195, pdf.get_y() + 0.5)
pdf.set_draw_color(0, 0, 0)
pdf.set_font("Arial", "", 10)
pdf.set_x(left)
pos_label = f"3D-Druck: {projekt}" if projekt else "3D-Druck"
pdf.cell(85, 7, pos_label)
pdf.cell(20, 7, str(stueckzahl), align='R')
pdf.cell(30, 7, f"{calc['verkaufspreis']:.2f} EUR", align='R')
pdf.cell(30, 7, f"{gesamt_netto:.2f} EUR", ln=True, align='R')
pdf.line(left, pdf.get_y() + 0.5, 195, pdf.get_y() + 0.5)
# --- Summen ---
pdf.set_x(left)
pdf.ln(8)
pdf.set_font("Arial", "", 10)
pdf.cell(130, 6, "Nettobetrag:")
pdf.cell(50, 6, f"{gesamt_netto:.2f} EUR", ln=True, align='R')
if calc.get('mwst_ausweisen'):
pdf.set_x(left)
pdf.cell(130, 6, "MwSt. (19%):")
pdf.cell(50, 6, f"{gesamt_mwst:.2f} EUR", ln=True, align='R')
pdf.set_font("Arial", "B", 12)
pdf.set_x(left)
label_total = "GESAMTBETRAG (brutto):" if calc.get('mwst_ausweisen') else "GESAMTBETRAG:"
pdf.cell(130, 8, label_total)
pdf.cell(50, 8, f"{gesamt_brutto:.2f} EUR", ln=True, align='R')
# --- Zahlungsbedingungen ---
pdf.set_font("Arial", "", 9)
pdf.set_x(left)
pdf.ln(6)
pdf.cell(0, 4, "Zahlbar innerhalb von 14 Tagen ohne Abzug. Vielen Dank fur Ihren Auftrag!")
# --- Footer ---
pdf.set_y(-35)
pdf.set_draw_color(180, 180, 180)
pdf.line(left, pdf.get_y(), 195, pdf.get_y())
pdf.set_draw_color(0, 0, 0)
pdf.ln(3)
pdf.set_font("Arial", "", 8)
pdf.set_text_color(100, 100, 100)
footer_parts = [firmen_name] if firmen_name else []
if telefon:
footer_parts.append(f"Tel: {telefon}")
if email:
footer_parts.append(email)
if footer_parts:
pdf.set_x(left)
pdf.cell(0, 4, " | ".join(footer_parts), ln=True)
if paypal:
pdf.set_x(left)
pdf.cell(0, 4, f"PayPal: {paypal}", ln=True)
if iban and bank_name:
pdf.set_x(left)
pdf.cell(0, 4, f"{iban} | {bank_name}", ln=True)
elif iban:
pdf.set_x(left)
pdf.cell(0, 4, iban, ln=True)
pdf.set_text_color(0, 0, 0)
return send_file(
io.BytesIO(pdf.output(dest='S').encode('latin-1')),
mimetype='application/pdf',
as_attachment=True,
download_name='rechnung.pdf'
)
@app.route('/export', methods=['POST'])
def export():
"""Exportiert die Kalkulation als TXT-Datei."""
data = request.get_json()
config = load_config()
merged = {**config, **data}
calc = calculate(merged)
lines = [
"3D-DRUCK KOSTENKALKULATION",
"=" * 50 + "\n",
f"Projekt: {data.get('projektName', '')}",
f"Datum: {data.get('datum', '')}\n",
"KOSTENAUFSTELLUNG:",
f"Materialkosten: {calc['materialkosten']:.2f} EUR",
f"Stromkosten: {calc['stromkosten']:.2f} EUR",
f"Abschreibung: {calc['abschreibung']:.2f} EUR",
f"Arbeitskosten: {calc['arbeitskosten']:.2f} EUR",
f"Gesamtkosten: {calc['gesamtkosten_netto']:.2f} EUR",
f"Gewinn: {calc['gewinn']:.2f} EUR",
f"Verkaufspreis (netto): {calc['verkaufspreis']:.2f} EUR",
]
if calc.get('mwst_ausweisen'):
lines.append(f"MwSt. (19%): {calc['mwst']:.2f} EUR")
lines.append(f"GESAMTPREIS: {calc['gesamt_brutto']:.2f} EUR")
text = '\n'.join(lines)
return send_file(
io.BytesIO(text.encode('utf-8')),
mimetype='text/plain',
as_attachment=True,
download_name='kalkulation.txt'
)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001, debug=True)