feat: MVP 3D-Druck Kostenkalkulator

- Single-Page HTML-App mit allen 18 Eingabefeldern
- 12 Berechnungen live (calc.js, reine Funktionen)
- LocalStorage-Persistenz, Mehrfach-Projekte via Sidebar
- Excel Im-/Export ueber SheetJS (vendored, MIT)
- Drag&Drop + File-Picker-Import
- Apple-Swiss-Styling, responsive
- Vorlagen-Excel mit 3 Reitern (Eingabe/Kalkulation/Angebot), Formeln referenzieren Eingabe
- openpyxl-Script fuer reproduzierbaren Template-Build
- 5 Test-Szenarien validiert

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 21:15:25 +02:00
commit 7507f768a3
12 changed files with 1308 additions and 0 deletions

View File

@@ -0,0 +1,211 @@
#!/usr/bin/env python3
"""Erzeugt die Vorlagen-Excel 3D-Druck-Kostenkalkulator.xlsx (ohne Makros).
Reiter:
- Eingabe: alle 18 Eingabefelder
- Kalkulation: Formeln fuer alle 12 Berechnungen (Zellbezuege zu Eingabe)
- Angebot: einfache Druck-Ansicht
Ausfuehrung: python3 scripts/generate-template.py
"""
from pathlib import Path
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.utils import get_column_letter
OUT = Path(__file__).resolve().parent.parent / "templates" / "3D-Druck-Kostenkalkulator.xlsx"
HEADER_FONT = Font(bold=True, color="FFFFFF", size=12)
HEADER_FILL = PatternFill("solid", fgColor="1D1D1F")
LABEL_FONT = Font(size=11)
VALUE_FONT = Font(size=11, bold=False)
RESULT_FONT = Font(size=12, bold=True)
PRIMARY_FILL = PatternFill("solid", fgColor="0071E3")
SUBTLE_FILL = PatternFill("solid", fgColor="F5F5F7")
EUR = '""#,##0.00'
PCT = '0.0"%"'
def style_header(ws, row, cols):
for c in range(1, cols + 1):
cell = ws.cell(row=row, column=c)
cell.font = HEADER_FONT
cell.fill = HEADER_FILL
cell.alignment = Alignment(horizontal="left", vertical="center")
def build_input(wb) -> None:
ws = wb.create_sheet("Eingabe")
ws.column_dimensions["A"].width = 42
ws.column_dimensions["B"].width = 20
ws.column_dimensions["C"].width = 16
ws["A1"] = "Feld"
ws["B1"] = "Wert"
ws["C1"] = "Einheit"
style_header(ws, 1, 3)
rows = [
("Projektname", "Beispiel-Projekt", ""),
("Kunde / Auftrag", "", ""),
("Materialtyp", "PLA", ""),
("Materialkosten pro kg", 25.00, "EUR/kg"),
("Materialverbrauch", 120, "g"),
("Druckzeit", 5.5, "h"),
("Maschinenstundensatz", 3.00, "EUR/h"),
("Stromverbrauch", 0.15, "kWh"),
("Strompreis", 0.35, "EUR/kWh"),
("Nachbearbeitungszeit", 15, "min"),
("Nachbearbeitungs-Stundensatz", 30.00, "EUR/h"),
("Verpackungskosten", 1.00, "EUR"),
("Versandkosten", 4.50, "EUR"),
("Ruestkosten", 2.00, "EUR"),
("Ausschussrisiko", 5, "%"),
("Gewinnaufschlag", 30, "%"),
("Stueckzahl", 1, "Stueck"),
("Individueller Zuschlag/Rabatt", 0, "EUR"),
("MwSt", 19, "%"),
("Notizen", "", ""),
]
for i, (label, value, unit) in enumerate(rows, start=2):
ws.cell(row=i, column=1, value=label).font = LABEL_FONT
ws.cell(row=i, column=2, value=value).font = VALUE_FONT
ws.cell(row=i, column=3, value=unit).font = LABEL_FONT
if i % 2 == 0:
for c in range(1, 4):
ws.cell(row=i, column=c).fill = SUBTLE_FILL
# Formate
ws["B5"].number_format = EUR # Materialkosten/kg
ws["B8"].number_format = EUR # Maschinenstundensatz
ws["B10"].number_format = EUR # Strompreis
ws["B12"].number_format = EUR # Nachbearb. Satz
ws["B13"].number_format = EUR # Verpackung
ws["B14"].number_format = EUR # Versand
ws["B15"].number_format = EUR # Ruestkosten
ws["B19"].number_format = EUR # Zuschlag/Rabatt
ws["B16"].number_format = PCT # Ausschuss
ws["B17"].number_format = PCT # Gewinnaufschlag
ws["B20"].number_format = PCT # MwSt
def build_calc(wb) -> None:
"""Formeln referenzieren Eingabe!B-Spalten.
Eingabe-Zuordnung (row in Eingabe):
B5 materialCostPerKg
B6 materialUsageG
B7 printTimeH
B8 machineRate
B9 powerKwh
B10 powerPrice
B11 postMin
B12 postRate
B13 packagingCost
B14 shippingCost
B15 setupCost
B16 scrapPct (%)
B17 marginPct (%)
B18 quantity
B19 individualAdjustment
B20 vatPct (%)
"""
ws = wb.create_sheet("Kalkulation")
ws.column_dimensions["A"].width = 6
ws.column_dimensions["B"].width = 40
ws.column_dimensions["C"].width = 22
ws["A1"] = "#"
ws["B1"] = "Position"
ws["C1"] = "Betrag (EUR)"
style_header(ws, 1, 3)
formulas = [
("1", "Materialkosten", "=Eingabe!B6*(Eingabe!B5/1000)"),
("2", "Maschinenkosten", "=Eingabe!B7*Eingabe!B8"),
("3", "Energiekosten", "=Eingabe!B7*Eingabe!B9*Eingabe!B10"),
("4", "Nachbearbeitungskosten", "=(Eingabe!B11/60)*Eingabe!B12"),
("5", "Gesamtherstellungskosten", "=C2+C3+C4+C5+Eingabe!B15+Eingabe!B13+Eingabe!B14"),
("6", "Ausschuss-Zuschlag", "=C6*(Eingabe!B16/100)"),
("7", "Zwischensumme netto", "=C6+C7"),
("8", "Marge", "=C8*(Eingabe!B17/100)"),
("9", "Kundenpreis netto", "=C8+C9+Eingabe!B19"),
("10", "Stueckpreis netto", "=C10/MAX(Eingabe!B18,1)"),
("11", "Stueckpreis brutto", "=C11*(1+Eingabe!B20/100)"),
("12", "Gesamtpreis brutto", "=C12*MAX(Eingabe!B18,1)"),
]
for i, (num, label, formula) in enumerate(formulas, start=2):
ws.cell(row=i, column=1, value=num).font = LABEL_FONT
ws.cell(row=i, column=2, value=label).font = LABEL_FONT
cell = ws.cell(row=i, column=3, value=formula)
cell.font = VALUE_FONT
cell.number_format = EUR
if i % 2 == 0:
for c in range(1, 4):
ws.cell(row=i, column=c).fill = SUBTLE_FILL
# Finale Hervorhebung fuer Stueckpreis brutto (Zeile 12 -> #11)
for c in range(1, 4):
ws.cell(row=12, column=c).fill = PRIMARY_FILL
ws.cell(row=12, column=c).font = Font(bold=True, color="FFFFFF", size=12)
def build_offer(wb) -> None:
ws = wb.create_sheet("Angebot")
for col, width in zip("ABCD", (32, 12, 22, 16)):
ws.column_dimensions[col].width = width
ws["A1"] = "ANGEBOT"
ws["A1"].font = Font(bold=True, size=20)
ws.merge_cells("A1:D1")
ws["A3"] = "Projekt"
ws["B3"] = "=Eingabe!B2"
ws["A4"] = "Kunde"
ws["B4"] = "=Eingabe!B3"
ws["A5"] = "Datum"
ws["B5"] = "=TEXT(TODAY(),\"DD.MM.YYYY\")"
for r in (3, 4, 5):
ws.cell(row=r, column=1).font = Font(bold=True)
ws["A7"] = "Position"
ws["B7"] = "Menge"
ws["C7"] = "Stueckpreis brutto"
ws["D7"] = "Gesamt"
style_header(ws, 7, 4)
ws["A8"] = "=Eingabe!B2"
ws["B8"] = "=Eingabe!B18"
ws["C8"] = "=Kalkulation!C12"
ws["C8"].number_format = EUR
ws["D8"] = "=Kalkulation!C13"
ws["D8"].number_format = EUR
ws["A10"] = "Gesamt brutto"
ws["A10"].font = Font(bold=True)
ws["D10"] = "=D8"
ws["D10"].font = Font(bold=True)
ws["D10"].number_format = EUR
ws["A12"] = "Hinweise"
ws["A12"].font = Font(bold=True)
ws["A13"] = "=Eingabe!B21"
def main() -> None:
wb = Workbook()
# Default-Sheet entfernen
wb.remove(wb.active)
build_input(wb)
build_calc(wb)
build_offer(wb)
OUT.parent.mkdir(parents=True, exist_ok=True)
wb.save(OUT)
print(f"Template geschrieben: {OUT}")
if __name__ == "__main__":
main()