Files
3ddruckkalkulation/AGENTS.md
Erik Thiele b0e6b1b9f2 JET-Design-Wortmarke im Kopf und auf der Rechnung
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>
2026-09-08 14:25:52 +02:00

116 lines
6.1 KiB
Markdown

# AGENTS.md — 3D-Druck Kostenkalkulation
## Project structure
- `app.py` — Flask web app
- `templates/index.html` — Hauptseite mit Formular und Live-Ergebnissen
- `templates/invoice.html` — Rechnungsseite mit PDF-Download
- `templates/settings.html` — Einstellungsseite (Druckerabschreibung, Arbeitskosten, Marge, MwSt., Firmendaten, Logo)
- `static/style.css` — Light-/Dark-Mode-Theming
- `static/script.js` — Live-Berechnung + Auto-Save beim Tippen
- `static/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-Ersteinrichtung
- `proxmox-update.sh` — Proxmox LXC-Update
- `.venv/` — virtual environment with all deps
## Setup
```bash
source .venv/bin/activate
```
**Dependencies**: gepinnt in `requirements.txt` (`pip install -r requirements.txt`).
`flask`, `fpdf2` (imported as `fpdf`), `PyYAML`. Alle in `.venv`.
## Run
```bash
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
- `fpdf` kommt via `fpdf2`-Paket, Import ist `from fpdf import FPDF`.
Die Legacy-API (`ln=True`, `output(dest='S')`, Font "Arial") ist abgeloest
durch `new_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()` wirft `ConfigError`, wenn die YAML kaputt ist. Nicht
abfangen und auf Defaults zurueckfallen -- das wuerde die Datei beim
naechsten Autosave endgueltig ueberschreiben.
- `/save_config` uebernimmt nur Keys aus `DEFAULT_VALUES`.
- **Steuerlogik**: `steuerregime` ist 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_ausweisen` ist davon *abgeleitet*
und wird in `_migriere()` gesetzt -- nie direkt schreiben. `_migriere()`
laeuft in `load_config()` VOR dem Auffuellen mit Defaults (sonst greift
die Ableitung aus Alt-Configs nicht) und in `/save_config` nach 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 aus `DEFAULT_POSITION`. `calculate()` liefert `calc['positionen']`
(je Position `stueckpreis` und `zeilensumme`) plus die Auftragssummen.
Jede Liste aus Config oder Client geht durch `saubere_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 liefert `null` auf Seiten ohne
Positionsliste (Einstellungen, Rechnung). Dann darf `positionen` NICHT
mitgesendet werden -- sonst wuerde ein Tastendruck dort die Positionen
loeschen.
- **Projekte**: `druckkalkulation_projekte.yaml` haelt benannte Staende,
Schluessel ist der Projektname. Nur `PROJEKT_FELDER` werden 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.yaml` ist der Arbeitsstand, nicht das Archiv.
- `/projekte/neu` setzt nur `PROJEKT_FELDER` zurueck. `GET /projekte`
liefert zusaetzlich `ungespeichert` -- der Vergleich laeuft ueber
`_vergleichbar()` und ignoriert bewusst `datum` und `geaendert`, sonst
waere ein Projekt am naechsten Tag faelschlich "ungespeichert".
- Alte Configs mit den Flachfeldern (`materialGewicht` etc.) migriert
`_migriere()` in eine einzelne Position und entfernt die Altfelder.
- **Saeamtliche UI-Texte sind Deutsch** — Labels, Fehlermeldungen, Rechnungsinhalte.
- Nur `.venv/`, `druckkalkulation_config.yaml` und `static/logo.*` werden ignoriert.
`static/img/logo/` ist davon nicht betroffen und wird versioniert.
- Das Rechnungslogo liefert `logo_datei()`: hochgeladenes Logo vor
`STANDARD_LOGO`, und erst wenn beide Dateien fehlen, setzt der PDF-Kopf
den Namen als Text. Nie direkt `config['logo']` fuer die Ausgabe lesen.
- Die Wortmarke ist im SVG fest eingefaerbt. Deshalb stehen im Kopf beide
Varianten im Markup und `static/style.css` blendet ueber
`[data-bs-theme="dark"]` die passende ein -- `toggleDarkMode()` laedt die
Seite nicht neu, eine serverseitige Auswahl waere danach falsch. Am
`navbar-brand` darf `navbar-brand-autodark` nicht stehen: Tabler invertiert
damit Bilder im Dark Mode und wuerde die weisse Variante schwarz faerben.
- `3dDruckKalkulator.py` ist die alte Tkinter-Version und wird nicht mehr verwendet (nicht im Tracking).