Die Logos stammen aus der Shop-Webseite (assets/img/logo). Im Seitenkopf stehen beide SVG-Varianten im Markup, CSS blendet die zum Theme passende ein -- der Dark-Mode-Schalter laedt die Seite nicht neu. Fuer den Rechnungsdruck liegt die Wortmarke zusaetzlich als JPEG bei, weil fpdf2 kein SVG setzen kann. logo_datei() waehlt aus: hochgeladenes Logo vor mitgelieferter Marke, Textkopf nur noch als Notnagel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.1 KiB
6.1 KiB
AGENTS.md — 3D-Druck Kostenkalkulation
Project structure
app.py— Flask web apptemplates/index.html— Hauptseite mit Formular und Live-Ergebnissentemplates/invoice.html— Rechnungsseite mit PDF-Downloadtemplates/settings.html— Einstellungsseite (Druckerabschreibung, Arbeitskosten, Marge, MwSt., Firmendaten, Logo)static/style.css— Light-/Dark-Mode-Themingstatic/script.js— Live-Berechnung + Auto-Save beim Tippenstatic/img/logo/— JET-Design-Wortmarke:jet-design.svg(dunkle Schrift),jet-design-weiss.svg(Dark Mode),jet-design.jpg(Rechnungs-PDF)druckkalkulation_config.yaml— auto-created/auto-saved state (in .gitignore)proxmox-setup.sh— Proxmox LXC-Ersteinrichtungproxmox-update.sh— Proxmox LXC-Update.venv/— virtual environment with all deps
Setup
source .venv/bin/activate
Dependencies: gepinnt in requirements.txt (pip install -r requirements.txt).
flask, fpdf2 (imported as fpdf), PyYAML. Alle in .venv.
Run
python app.py # Debug aus, lauscht auf 0.0.0.0:5001
FLASK_DEBUG=1 python app.py # Debug an, dann nur 127.0.0.1
HOST und PORT ueberschreiben die Defaults.
Kein Test, Lint, Typecheck oder CI vorhanden. README.md und AGENTS.md enthalten die Projektdokumentation.
Routes
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | / |
Hauptseite mit Formular |
| POST | /calculate |
JSON-Berechnung (AJAX) |
| POST | /save_config |
Speichert Werte in YAML |
| GET | /invoice |
Rechnungsseite |
| POST | /invoice/pdf |
PDF-Download |
| POST | /export |
TXT-Download |
| GET | /settings |
Einstellungsseite (Druckerabschreibung, Arbeitskosten, Marge, MwSt.) |
| POST | /upload_logo |
Logo hochladen (PNG/JPEG/WebP) |
| POST | /delete_logo |
Logo loschen |
Config
druckkalkulation_config.yaml wird beim Start gelesen und bei jeder Eingabe automatisch gespeichert (debounced 500ms via AJAX). Enthaelt alle Formularwerte plus dark_mode. Wird automatisch erstellt — kein manuelles Setup.
Wichtige Fallstricke
fpdfkommt viafpdf2-Paket, Import istfrom fpdf import FPDF. Die Legacy-API (ln=True,output(dest='S'), Font "Arial") ist abgeloest durchnew_x/new_y,output()und "Helvetica" -- nicht zurueckbauen.- PDF-Kernfonts koennen nur latin-1. Jeder Freitext muss durch
pdf_text(), sonst wirft die PDF-Route bei Sonderzeichen einen UnicodeEncodeError. load_config()wirftConfigError, wenn die YAML kaputt ist. Nicht abfangen und auf Defaults zurueckfallen -- das wuerde die Datei beim naechsten Autosave endgueltig ueberschreiben./save_configuebernimmt nur Keys ausDEFAULT_VALUES.- Steuerlogik:
steuerregimeist die Quelle der Wahrheit und kennt drei Werte (STEUERREGIME): 'privat' | 'kleinunternehmer' | 'regel'. 'privat' ist rechtlich NICHT dasselbe wie 'kleinunternehmer': ein Privatverkaeufer ist kein Unternehmer nach §2 UStG, daher duerfen weder §19-Hinweis noch Steuernummer/USt-IdNr. auf den Beleg -- auch dann nicht, wenn die Felder in der Config gefuellt sind (calc['ist_unternehmer']).mwst_ausweisenist davon abgeleitet und wird in_migriere()gesetzt -- nie direkt schreiben._migriere()laeuft inload_config()VOR dem Auffuellen mit Defaults (sonst greift die Ableitung aus Alt-Configs nicht) und in/save_confignach dem Update (sonst steht Widerspruechliches auf der Platte). - Der MwSt.-Satz kommt aus
mwst_satz, nicht mehr hartcodiert. - Positionen:
config['positionen']ist eine Liste von Dicts mit den Feldern ausDEFAULT_POSITION.calculate()liefertcalc['positionen'](je Positionstueckpreisundzeilensumme) plus die Auftragssummen. Jede Liste aus Config oder Client geht durchsaubere_positionen()-- nie ungeprueft uebernehmen. Es gibt immer mindestens eine Position. - Die Auftragssumme ist die Summe der bereits gerundeten Zeilen, damit die Rechnung zeilenweise nachrechenbar bleibt.
sammlePositionen()im Frontend liefertnullauf Seiten ohne Positionsliste (Einstellungen, Rechnung). Dann darfpositionenNICHT mitgesendet werden -- sonst wuerde ein Tastendruck dort die Positionen loeschen.- Projekte:
druckkalkulation_projekte.yamlhaelt benannte Staende, Schluessel ist der Projektname. NurPROJEKT_FELDERwerden gesichert -- Betriebswerte (Marge, Steuer, Drucker, Firmendaten) gehoeren bewusst NICHT dazu, sonst wuerde das Laden eines alten Projekts sie ueberschreiben. Eigenes Lock_projekte_lock, gleiche atomare Schreibtechnik wie die Config. Die Preise in der Projektliste werden mit den AKTUELLEN Betriebswerten gerechnet, nicht mit denen von damals. druckkalkulation_config.yamlist der Arbeitsstand, nicht das Archiv./projekte/neusetzt nurPROJEKT_FELDERzurueck.GET /projekteliefert zusaetzlichungespeichert-- der Vergleich laeuft ueber_vergleichbar()und ignoriert bewusstdatumundgeaendert, sonst waere ein Projekt am naechsten Tag faelschlich "ungespeichert".- Alte Configs mit den Flachfeldern (
materialGewichtetc.) migriert_migriere()in eine einzelne Position und entfernt die Altfelder. - Saeamtliche UI-Texte sind Deutsch — Labels, Fehlermeldungen, Rechnungsinhalte.
- Nur
.venv/,druckkalkulation_config.yamlundstatic/logo.*werden ignoriert.static/img/logo/ist davon nicht betroffen und wird versioniert. - Das Rechnungslogo liefert
logo_datei(): hochgeladenes Logo vorSTANDARD_LOGO, und erst wenn beide Dateien fehlen, setzt der PDF-Kopf den Namen als Text. Nie direktconfig['logo']fuer die Ausgabe lesen. - Die Wortmarke ist im SVG fest eingefaerbt. Deshalb stehen im Kopf beide
Varianten im Markup und
static/style.cssblendet ueber[data-bs-theme="dark"]die passende ein --toggleDarkMode()laedt die Seite nicht neu, eine serverseitige Auswahl waere danach falsch. Amnavbar-branddarfnavbar-brand-autodarknicht stehen: Tabler invertiert damit Bilder im Dark Mode und wuerde die weisse Variante schwarz faerben. 3dDruckKalkulator.pyist die alte Tkinter-Version und wird nicht mehr verwendet (nicht im Tracking).