Bisher liess sich ein neuer Auftrag nur von Hand beginnen: Namen ueberschreiben, jede Position neu ausfuellen und auf der Rechnungsseite Kundendaten und Rechnungsnummer leeren. Wurde der letzte Schritt vergessen, stand der alte Kunde auf der neuen Rechnung. - POST /projekte/neu setzt nur PROJEKT_FELDER zurueck (Name, Positionen, Kundendaten, Rechnungsnummer, Datum auf heute). Betriebswerte wie Marge, Stundenlohn, Drucker, Steuer und Firmendaten bleiben. - GET /projekte liefert zusaetzlich 'ungespeichert'. Der Knopf fragt nur nach, wenn der offene Stand wirklich nicht als Projekt gesichert ist. - Der Vergleich ignoriert 'datum' und 'geaendert', sonst waere ein gespeichertes Projekt am naechsten Tag faelschlich "ungespeichert". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.3 KiB
5.3 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 Tippendruckkalkulation_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. 3dDruckKalkulator.pyist die alte Tkinter-Version und wird nicht mehr verwendet (nicht im Tracking).