Flask-Web-App statt Tkinter: Live-Berechnung, PDF-Rechnung, Dark Mode, Tabler CSS
This commit is contained in:
237
app.py
Normal file
237
app.py
Normal file
@@ -0,0 +1,237 @@
|
||||
"""
|
||||
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,
|
||||
'rechnungsnummer': '',
|
||||
'datum': datetime.now().strftime('%Y-%m-%d'),
|
||||
'dark_mode': False,
|
||||
}
|
||||
|
||||
|
||||
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 = verkaufspreis * 0.19
|
||||
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),
|
||||
'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('/calculate', methods=['POST'])
|
||||
def calc_json():
|
||||
"""AJAX-Endpunkt: Nimmt Formular-JSON entgegen, gibt Berechnung zurueck."""
|
||||
data = request.get_json()
|
||||
calc = calculate(data)
|
||||
return jsonify(calc)
|
||||
|
||||
|
||||
@app.route('/save_config', methods=['POST'])
|
||||
def save_config_route():
|
||||
"""AJAX-Endpunkt: Speichert aktuelle Formularwerte in YAML."""
|
||||
data = request.get_json()
|
||||
save_config(data)
|
||||
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)
|
||||
|
||||
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: {data.get('rechnungsnummer', config.get('rechnungsnummer', ''))}", ln=True)
|
||||
pdf.cell(0, 6, f"Datum: {data.get('datum', config.get('datum', ''))}", ln=True)
|
||||
pdf.ln(10)
|
||||
|
||||
pdf.set_font("Arial", "B", 12)
|
||||
pdf.cell(0, 6, "Rechnungsempfanger:", ln=True)
|
||||
pdf.set_font("Arial", "", 11)
|
||||
pdf.cell(0, 6, data.get('kundenName', config.get('kundenName', '')), ln=True)
|
||||
pdf.multi_cell(0, 6, data.get('kundenAdresse', config.get('kundenAdresse', '')))
|
||||
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: {config.get('projektName', '')}")
|
||||
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 fur Ihren Auftrag!")
|
||||
|
||||
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()
|
||||
calc = calculate(data)
|
||||
|
||||
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",
|
||||
f"MwSt. (19%): {calc['mwst']:.2f} EUR",
|
||||
f"GESAMTPREIS (brutto): {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(debug=True)
|
||||
Reference in New Issue
Block a user