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:
211
scripts/generate-template.py
Normal file
211
scripts/generate-template.py
Normal 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()
|
||||
Reference in New Issue
Block a user