44 KiB
ERP Nutzfahrzeuge – Architekturdokument
Version: 1.0
Datum: 2026-07-12
Status: Approved for Implementation
Solution Architect: A0 Orchestrator
1. Übersicht
ERP-System für den Handel mit Nutzfahrzeugen (LKW, Baumaschinen, PKW, Stapler, Transporter). Backend in Python/FastAPI, Frontend in React/Next.js (mobile-first), PostgreSQL als Datenbank, Redis für Caching/Queues. Hosting via Coolify self-hosted mit Docker-Compose (4 Container).
1.1 Stakeholder & Rollen
| Rolle | Berechtigungen | Nutzer (~10) |
|---|---|---|
| Admin | Vollzugriff, User-Management, Systemeinstellungen | 1-2 |
| Verkäufer | Fahrzeuge, Kontakte, Verkäufe, OCR, KI-Copilot | 5-7 |
| Buchhaltung | Verkäufe (read), DATEV-Export, USt-IdNr.-Prüfung | 1-2 |
1.2 Sprachen
- Deutsch (DE) – Primärsprache
- Englisch (EN) – Sekundärsprache
2. Tech-Stack
| Schicht | Technologie | Version |
|---|---|---|
| Backend | Python / FastAPI | Python 3.12, FastAPI 0.111+ |
| Frontend | React / Next.js | Next.js 15 (App Router), React 19 |
| CSS | Tailwind CSS | 3.4+ |
| Datenbank | PostgreSQL | 16+ |
| Cache/Queue | Redis | 7+ |
| ORM | SQLAlchemy 2.0 + Alembic | async |
| KI | OpenRouter API | Qwen2.5-VL (OCR), Flux.1-Pro (Bild), Claude/GPT-4 (Copilot) |
| mobile.de | Seller API (Push only) | REST |
| OCR | OpenRouter Vision (Qwen2.5-VL) | – |
| DATEV | CSV-Export (DATEV-Format) | – |
| USt-IdNr. | BZSt API (geplant, aktuell kein Zugang) | REST |
| Deployment | Docker-Compose via Coolify | 4 Container |
| Auth | JWT (access + refresh) | – |
| Testing | pytest + httpx (Backend), Vitest + Playwright (Frontend) | – |
3. Deployment-Architektur
3.1 Docker-Compose (4 Container)
┌─────────────────────────────────────────────────┐
│ Coolify (46.225.91.159) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ frontend │ │ backend │ │ redis │ │
│ │ Next.js │ │ FastAPI │ │ cache │ │
│ │ :3000 │ │ :8000 │ │ :6379 │ │
│ └────┬─────┘ └────┬─────┘ └──────────┘ │
│ │ │ │
│ │ ┌────┴─────┐ │
│ │ │ postgres │ │
│ │ │ :5432 │ │
│ │ └──────────┘ │
│ │ │
│ Traefik (SSL, Let's Encrypt) │
└─────────────────────────────────────────────────┘
3.2 Netzwerk
- Traefik als Reverse Proxy mit automatischem SSL
- Frontend erreichbar unter
https://erp.media-on.de - Backend API unter
https://erp.media-on.de/api - Interne Kommunikation über Docker-Netzwerk
3.3 Volumes
postgres_data: Datenbank-Persistenzredis_data: Redis-Persistenz (optional)uploads_data: Fahrzeug-Dateien (Bilder, Dokumente, OCR-Scans)retouched_images: Retuschierte Bilder
4. Modulstruktur
4.1 Backend-Struktur (FastAPI)
backend/
├── app/
│ ├── main.py # FastAPI app entry, CORS, routers
│ ├── config.py # Settings (Pydantic BaseSettings)
│ ├── database.py # async engine, session factory
│ ├── dependencies.py # Common deps (auth, pagination)
│ ├── models/ # SQLAlchemy models
│ │ ├── user.py
│ │ ├── vehicle.py
│ │ ├── contact.py
│ │ ├── sale.py
│ │ ├── document.py
│ │ └── ocr_result.py
│ ├── schemas/ # Pydantic schemas (request/response)
│ │ ├── user.py
│ │ ├── vehicle.py
│ │ ├── contact.py
│ │ ├── sale.py
│ │ └── ocr.pyn│ ├── routers/ # API route handlers
│ │ ├── auth.py
│ │ ├── vehicles.py
│ │ ├── contacts.py
│ │ ├── sales.py
│ │ ├── ocr.py
│ │ ├── files.py
│ │ ├── copilot.py
│ │ └── image_retouch.py
│ ├── services/ # Business logic
│ │ ├── auth_service.py
│ │ ├── vehicle_service.py
│ │ ├── mobilede_service.py
│ │ ├── contact_service.py
│ │ ├── sale_service.py
│ │ ├── ocr_service.py
│ │ ├── file_service.py
│ │ ├── copilot_service.py
│ │ ├── retouch_service.py
│ │ └── datev_service.py
│ ├── tasks/ # Background tasks (async)
│ │ ├── mobilede_push.py
│ │ └── ocr_processing.py
│ └── utils/
│ ├── openrouter.py # OpenRouter API client
│ ├── i18n.py # Backend i18n helpers
│ └── datev.py # DATEV export formatter
├── alembic/ # DB migrations
├── tests/ # pytest test suite
├── requirements.txt
├── Dockerfile
└── pyproject.toml
4.2 Frontend-Struktur (Next.js)
frontend/
├── app/ # App Router
│ ├── layout.tsx # Root layout (i18n provider, auth)
│ ├── page.tsx # Dashboard
│ ├── (auth)/
│ │ └── login/page.tsx
│ ├── vehicles/
│ │ ├── page.tsx # List + filter
│ │ ├── [id]/page.tsx # Detail
│ │ └── new/page.tsx # Create form
│ ├── contacts/
│ │ ├── page.tsx # List
│ │ ├── [id]/page.tsx # Detail
│ │ └── new/page.tsx # Create form
│ ├── sales/
│ │ ├── page.tsx # List
│ │ ├── [id]/page.tsx # Detail + contract
│ │ └── new/page.tsx # Create sale
│ ├── ocr/
│ │ └── page.tsx # OCR upload + results
│ ├── copilot/
│ │ └── page.tsx # KI-Copilot chat
│ └── settings/
│ └── page.tsx # Admin settings
├── components/ # Reusable components
│ ├── ui/ # Base UI (Button, Input, Card, Table)
│ ├── vehicles/ # Vehicle-specific components
│ ├── contacts/ # Contact-specific components
│ ├── sales/ # Sale-specific components
│ ├── ocr/ # OCR components
│ └── copilot/ # Copilot chat components
├── lib/ # Client utilities
│ ├── api.ts # API client (fetch wrapper)
│ ├── auth.ts # Auth context/hooks
│ └── i18n.ts # i18n config
├── messages/ # i18n translation files
│ ├── de.json
│ └── en.json
├── public/
├── Dockerfile
├── tailwind.config.ts
└── package.json
5. Datenmodell
5.1 Entity-Relationship-Übersicht
User ──< Vehicle ──< File (Dateiablage)
│ │
│ ├──< OCRResult
│ ├──< RetouchedImage
│ └──< Sale ──< ContractDocument
│ └──< DATEVExport
│
└──< Contact (Kunde/Lieferant)
5.2 Kern-Entitäten
User
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| VARCHAR(255) | unique, not null | |
| full_name | VARCHAR(200) | not null |
| role | ENUM('admin','verkaeufer','buchhaltung') | not null |
| password_hash | VARCHAR(255) | not null |
| is_active | BOOLEAN | default true |
| created_at | TIMESTAMPTZ | default now() |
Vehicle
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| vehicle_type | ENUM('lkw','baumaschine','pkw','stapler','transporter') | not null |
| brand | VARCHAR(100) | not null |
| model | VARCHAR(150) | not null |
| vin | VARCHAR(17) | unique |
| registration_plate | VARCHAR(20) | |
| first_registration | DATE | |
| mileage_km | INTEGER | |
| power_kw | INTEGER | |
| fuel_type | ENUM('diesel','petrol','electric','hybrid','other') | |
| purchase_price | DECIMAL(12,2) | not null |
| sale_price | DECIMAL(12,2) | |
| status | ENUM('in_stock','reserved','sold','deleted') | default 'in_stock' |
| mobilede_ad_id | VARCHAR(50) | |
| mobilede_synced_at | TIMESTAMPTZ | |
| notes | TEXT | |
| created_by | UUID | FK → User |
| created_at | TIMESTAMPTZ | default now() |
| updated_at | TIMESTAMPTZ | default now() |
Contact
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| contact_type | ENUM('kunde','lieferant','beide') | not null |
| company_name | VARCHAR(200) | |
| first_name | VARCHAR(100) | |
| last_name | VARCHAR(100) | |
| street | VARCHAR(255) | |
| zip_code | VARCHAR(10) | |
| city | VARCHAR(100) | |
| country | VARCHAR(2) | default 'DE' |
| is_eu | BOOLEAN | default false |
| ust_id_nr | VARCHAR(20) | |
| phone | VARCHAR(30) | |
| VARCHAR(255) | ||
| created_at | TIMESTAMPTZ | default now() |
Sale
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| vehicle_id | UUID | FK → Vehicle, not null |
| buyer_contact_id | UUID | FK → Contact, not null |
| seller_user_id | UUID | FK → User, not null |
| sale_price | DECIMAL(12,2) | not null |
| sale_date | DATE | not null |
| payment_method | ENUM('bank_transfer','cash','financing','other') | |
| is_gwg | BOOLEAN | default false (Geringwertiges Wirtschaftsgut) |
| gwg_amount | DECIMAL(12,2) | |
| vat_rate | DECIMAL(5,2) | default 19.00 |
| ust_id_nr_verified | BOOLEAN | default false |
| contract_pdf_path | VARCHAR(500) | |
| datev_export_id | UUID | FK → DATEVExport (nullable) |
| created_at | TIMESTAMPTZ | default now() |
File (Dateiablage pro Fahrzeug)
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| vehicle_id | UUID | FK → Vehicle, not null |
| file_type | ENUM('image','document','ocr_scan','retouched_image') | not null |
| original_filename | VARCHAR(255) | not null |
| stored_path | VARCHAR(500) | not null |
| mime_type | VARCHAR(100) | not null |
| file_size_bytes | BIGINT | not null |
| uploaded_by | UUID | FK → User |
| created_at | TIMESTAMPTZ | default now() |
OCRResult
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| vehicle_id | UUID | FK → Vehicle, not null |
| file_id | UUID | FK → File, not null |
| raw_text | TEXT | extracted text |
| structured_data | JSONB | parsed fields (brand, model, vin, etc.) |
| ocr_type | ENUM('zb_i','zb_ii') | not null |
| confidence_score | FLOAT | |
| status | ENUM('pending','completed','failed') | default 'pending' |
| created_at | TIMESTAMPTZ | default now() |
DATEVExport
| Feld | Typ | Constraints |
|---|---|---|
| id | UUID | PK |
| export_date | DATE | not null |
| period_from | DATE | not null |
| period_to | DATE | not null |
| file_path | VARCHAR(500) | generated CSV path |
| record_count | INTEGER | |
| created_by | UUID | FK → User |
| created_at | TIMESTAMPTZ | default now() |
6. API Design
6.1 Auth
| Method | Endpoint | Beschreibung |
|---|---|---|
| POST | /api/auth/login |
Login → JWT (access + refresh) |
| POST | /api/auth/refresh |
Token refresh |
| GET | /api/auth/me |
Aktuellen User abrufen |
| PUT | /api/auth/me |
Profil aktualisieren |
6.2 Vehicles (M1)
| Method | Endpoint | Beschreibung |
|---|---|---|
| GET | /api/vehicles |
List with pagination, filter (type, status, brand), sort |
| GET | /api/vehicles/:id |
Detail view |
| POST | /api/vehicles |
Create new vehicle |
| PUT | /api/vehicles/:id |
Update vehicle |
| DELETE | /api/vehicles/:id |
Soft delete (status='deleted') |
| POST | /api/vehicles/:id/mobilede-push |
Push to mobile.de Seller API |
| GET | /api/vehicles/:id/mobilede-status |
Sync status abrufen |
6.3 OCR (M2)
| Method | Endpoint | Beschreibung |
|---|---|---|
| POST | /api/ocr/upload |
Upload ZB I/II scan → async OCR processing |
| GET | /api/ocr/results/:id |
OCR result abrufen |
| GET | /api/ocr/results?vehicle_id=X |
OCR results für Fahrzeug |
| POST | /api/ocr/results/:id/apply |
OCR-Daten auf Vehicle anwenden |
6.4 Contacts (M3)
| Method | Endpoint | Beschreibung |
|---|---|---|
| GET | /api/contacts |
List with pagination, filter (type, eu/inland), search |
| GET | /api/contacts/:id |
Detail view |
| POST | /api/contacts |
Create contact |
| PUT | /api/contacts/:id |
Update contact |
| DELETE | /api/contacts/:id |
Delete contact |
6.5 Files (M4)
| Method | Endpoint | Beschreibung |
|---|---|---|
| GET | /api/vehicles/:id/files |
List files for vehicle |
| POST | /api/vehicles/:id/files |
Upload file (multipart) |
| GET | /api/vehicles/:id/files/:fileId |
Download file |
| DELETE | /api/vehicles/:id/files/:fileId |
Delete file |
6.6 Sales (M5)
| Method | Endpoint | Beschreibung |
|---|---|---|
| GET | /api/sales |
List with pagination, filter (date, status) |
| GET | /api/sales/:id |
Detail view |
| POST | /api/sales |
Create sale (generates contract PDF) |
| PUT | /api/sales/:id |
Update sale |
| DELETE | /api/sales/:id |
Cancel/delete sale |
| POST | /api/sales/:id/contract |
Re-generate contract PDF |
| GET | /api/sales/:id/contract |
Download contract PDF |
| POST | /api/sales/:id/verify-ust-id |
USt-IdNr. BZSt API Prüfung (geplant) |
| POST | /api/datev/export |
DATEV CSV Export für Zeitraum |
| GET | /api/datev/exports |
List DATEV exports |
| GET | /api/datev/exports/:id/download |
Download DATEV CSV |
6.7 KI-Copilot (M7)
| Method | Endpoint | Beschreibung |
|---|---|---|
| POST | /api/copilot/chat |
Text chat → OpenRouter (Claude/GPT-4) |
| POST | /api/copilot/voice |
Voice input (STT) → chat response |
| GET | /api/copilot/history |
Chat history (paginated) |
| POST | /api/copilot/action |
System action ausführen (vehicle create, search, etc.) |
6.8 Image Retouch (M8)
| Method | Endpoint | Beschreibung |
|---|---|---|
| POST | /api/retouch/process |
Bild → Flux.1-Pro Retusche |
| GET | /api/retouch/results/:id |
Retuschiertes Bild abrufen |
| POST | /api/retouch/price-compare |
Preisvergleich mit mobile.de Listings |
6.9 Users (Admin)
| Method | Endpoint | Beschreibung |
|---|---|---|
| GET | /api/users |
List users (admin only) |
| POST | /api/users |
Create user (admin only) |
| PUT | /api/users/:id |
Update user (admin only) |
| DELETE | /api/users/:id |
Deactivate user (admin only) |
7. Test-Strategie
7.1 Backend (pytest)
- Unit Tests: Services isoliert mit Mocks (OpenRouter, mobile.de API)
- Integration Tests: API Endpoints mit Test-DB (SQLite/PostgreSQL testcontainer)
- Coverage Target: ≥ 80% pro Modul
- Fixtures: DB session, test client, mock OpenRouter responses
7.2 Frontend (Vitest + Playwright)
- Unit Tests: Komponenten mit Vitest + React Testing Library
- E2E Tests: Playwright für kritische User-Flows (Login, Vehicle CRUD, OCR, Sale)
- Coverage Target: ≥ 70% pro Komponentengruppe
7.3 Test-Commands
# Backend
cd backend && pytest --cov=app --cov-report=term-missing
# Frontend
cd frontend && npm run test
cd frontend && npm run e2e
# Full integration
docker compose -f docker-compose.test.yml up --abort-on-container-exit
8. Integrationsarchitektur
8.1 OpenRouter KI-Pipeline
User Request → FastAPI → OpenRouter API → Response
│
├── OCR: Qwen2.5-VL (Vision) → structured JSON
├── Retouch: Flux.1-Pro → Image URL/base64
└── Copilot: Claude/GPT-4 → Text/Action JSON
- API-Key via Environment Variable
OPENROUTER_API_KEY - Rate-Limiting via Redis Token Bucket
- Fehlerbehandlung: Retry mit exponential backoff (max 3)
- Response-Caching für identische OCR-Scans
8.2 mobile.de Seller API
Vehicle Update → FastAPI → mobile.de Seller API (POST/PUT)
↓
Ad ID gespeichert in Vehicle.mobilede_ad_id
- Nur Push-Richtung (kein Import von mobile.de)
- Auth: OAuth2 oder API-Key (in mobile.de Seller Portal konfigurierbar)
- Mapping: ERP Vehicle → mobile.de Ad Format
- Retry-Queue bei Fehlern (Redis)
- Status-Tracking:
mobilede_synced_at,mobilede_ad_id
8.3 DATEV-Export
Buchhaltung User → DATEV Export Request → FastAPI
→ Sammle Sales im Zeitraum → Format CSV (DATEV-Format) → Download
- Format: DATEV CSV (Buchungsstapel)
- Felder: Datum, Konto, Gegenkonto, Betrag, Belegfeld, Buchungstext
- GwG-Behandlung: Sofortabzug bei ≤ 800€ (§6 Abs. 2 EStG)
- USt-IdNr.-Prüfung: BZSt API (geplant, Feature-Flag
bzst_api_enabled)
9. ADRs (Architecture Decision Records)
ADR-001: FastAPI statt Django
Status: Accepted
Entscheidung: FastAPI als Backend-Framework.
Begründung: Async-native, bessere Performance, automatische OpenAPI-Dokumentation,
lichtgewichtiger als Django, native Pydantic-Integration.
Alternativen: Django REST Framework (zu schwerfällig), Flask (kein async).
ADR-002: Next.js App Router statt Pages Router
Status: Accepted
Entscheidung: Next.js 15 mit App Router.
Begründung: Server Components, Streaming, bessere Code-Organisation,
Zukunftssicherheit.
Alternativen: Pages Router (deprecated), CRA (deprecated).
ADR-003: OpenRouter als KI-Gateway
Status: Accepted
Entscheidung: Alle KI-Aufrufe über OpenRouter API.
Begründung: Ein API-Key für multiple Modelle, einheitliche API,
kein Vendor Lock-in, schnelle Modellwechsel.
Alternativen: Direkte OpenAI/Anthropic API (Vendor Lock-in),
lokale Modelle (zu langsam für OCR).
ADR-004: OCR via Vision-Modell statt Tesseract
Status: Accepted
Entscheidung: OCR über OpenRouter Qwen2.5-VL Vision-Modell.
Begründung: Bessere Erkennung bei handschriftlichen und undeutlichen ZB-Scans,
keine Infrastruktur für Tesseract nötig, strukturierbare JSON-Ausgabe.
Alternativen: Tesseract (schlechte Qualität bei Scans),
Google Cloud Vision (Privacy-Bedenken, kostenpflichtig).
ADR-005: Soft-Delete für Vehicles
Status: Accepted
Entscheidung: Vehicles werden nicht physisch gelöscht, sondern status='deleted'.
Begründung: Referentielle Integrität (Sales, Files, OCR Results),
Audit-Trail für Buchhaltung.
Alternativen: Hard Delete (Datenverlust, FK-Constraints).
ADR-006: JWT statt Session-based Auth
Status: Accepted
Entscheidung: JWT (access + refresh token) für Authentifizierung.
Begründung: Stateless, geeignet für API + SPA, keine Server-Side-Session nötig.
Alternativen: Session-Cookies (CSRF-Risiko, Server-State nötig).
ADR-007: Redis für Caching und Background Queues
Status: Accepted
Entscheidung: Redis als Cache und Task-Queue.
Begründung: Schnelles In-Memory-Caching für OCR-Results und mobile.de-Status,
Background-Queues für async OCR und mobile.de Push.
Alternativen: Celery+RabbitMQ (zu komplex für ~10 Nutzer),
keine Queue (blocking API).
ADR-008: Docker-Compose mit 4 Containern statt K8s
Status: Accepted
Entscheidung: Docker-Compose mit frontend, backend, postgres, redis.
Begründung: Einfach zu deployen via Coolify, ausreichend für ~10 Nutzer,
keine K8s-Komplexität.
Alternativen: Kubernetes (overkill), einzelne Coolify Services
(mehr Konfigurationsaufwand).
10. Nicht-funktionale Anforderungen
10.1 Performance
- API-Response < 500ms für Standard-CRUD (ohne KI-Calls)
- OCR-Verarbeitung < 30s pro Scan (async, User wird benachrichtigt)
- Bildretusche < 60s pro Bild (async)
- Frontend LCP < 2.5s (mobile.de Performance Budget)
10.2 Sicherheit
- JWT mit kurzer Access-Token-Gültigkeit (15min) + Refresh (7d)
- Role-based Access Control (RBAC) auf Endpoint-Ebene
- Input-Validierung via Pydantic auf allen Endpoints
- SQL-Injection-Schutz durch SQLAlchemy parameterized queries
- File-Upload: MIME-Type-Validierung, max 20MB pro Datei
- OpenRouter API-Key in Environment Variables, nie im Code
- HTTPS-only via Traefik + Let's Encrypt
10.3 Skalierbarkeit
- Horizontal skalierbar (stateless Backend, sticky sessions nicht nötig)
- Redis als verteiltes Cache → mehrere Backend-Instanzen möglich
- PostgreSQL Connection Pooling via SQLAlchemy async engine
- Für ~10 Nutzer: Single-Instance ausreichend
10.4 Verfügbarkeit
- Docker-Compose mit
restart: unless-stopped - PostgreSQL mit WAL-Archiving für Point-in-Time-Recovery
- Daily Backup via Coolify (PostgreSQL dump)
- Health-Checks auf
/api/health(Backend) und/(Frontend)
11. Risiken & Mitigation
| Risiko | Wahrscheinlichkeit | Impact | Mitigation |
|---|---|---|---|
| OpenRouter API-Ausfall | Mittel | Hoch | Retry mit Backoff, Fallback-Modell, Cache |
| mobile.de API-Änderung | Niedrig | Mittel | API-Version pinnen, Monitoring |
| OCR-Qualität unzureichend | Mittel | Mittel | Confidence-Score → Manual Review bei < 0.7 |
| BZSt API kein Zugang | Hoch | Niedrig | Feature-Flag, manuelle Prüfung als Fallback |
| Datenverlust PostgreSQL | Niedrig | Kritisch | Daily Backups, WAL-Archiving |
| Token-Kosten OpenRouter | Mittel | Mittel | Caching, Rate-Limiting pro User |
12. i18n Architektur
- Backend: Fehlermeldungen auf Deutsch/Englisch via Accept-Language Header
- Frontend: next-intl oder react-i18next
- Translation Files:
frontend/messages/de.json,frontend/messages/en.json - Sprachumschaltung: User-Settings → Locale in localStorage + Cookie
- Backend-Fehler: Error-Codes + lokalisierte Messages
13. Konventionen
14. PDF-Generierung & Briefpapier (Letterhead)
14.1 Library-Wahl: WeasyPrint
Entscheidung: WeasyPrint als PDF-Generierungs-Library.
Begründung: WeasyPrint rendert HTML/CSS → PDF. Dadurch können PDF-Templates
mit Standard-Web-Technologien (HTML, CSS, Jinja2) erstellt werden. Briefpapier
(Letterhead) wird über CSS @page Rules mit @top/@bottom Margin-Boxes
umgesetzt – inkl. Logo, Firmenadresse, Footer, Seitenzahlen.
Vergleich:
| Kriterium | WeasyPrint | ReportLab |
|---|---|---|
| Template-Erstellung | HTML/CSS + Jinja2 | Python-Code (Programmatic) |
| Briefpapier/Header/Footer | CSS @page Rules, repeating on every page | PageTemplate + Frame + onPage callback |
| Logo/Bild-Embedding | <img> oder CSS background-image |
Image() flowable |
| Lernkurve | Niedrig (Web-Entwickler kennen HTML/CSS) | Mittel (ReportLab-spezifische API) |
| Flexibilität | Hoch (volle CSS-Unterstützung) | Mittel (eingeschränkte Layout-Optionen) |
| Wartbarkeit | Hoch (Templates separiert von Code) | Niedrig (Layout im Python-Code) |
Alternativen: ReportLab (pixel-präzise Kontrolle, aber komplexer), fpdf2 (einfach aber weniger Features), wkhtmltopdf (veraltet).
14.2 Briefpapier-Implementierung
Template-Struktur:
backend/app/utils/pdf/
├── letterhead_template.html # Jinja2 HTML-Template mit Briefpapier
├── letterhead_style.css # CSS mit @page Rules (Header/Footer/Logo)
├── contract_template.html # Vertrag-Template (erbt Briefpapier)
├── invoice_template.html # Rechnung-Template (erbt Briefpapier)
└── pdf_generator.py # WeasyPrint Wrapper-Service
CSS @page für Briefpapier:
@page {
size: A4;
margin: 120px 40px 80px 40px; /* top right bottom left */
@top-left {
content: url('/data/letterhead/logo.png');
width: 180px;
}
@top-right {
content: 'Firmenname GmbH';
font-size: 9pt;
color: #666;
}
@bottom-left {
content: 'Firmenname GmbH · Straßenname 1 · 12345 Stadt';
font-size: 8pt;
color: #999;
}
@bottom-right {
content: 'Seite ' counter(page) ' von ' counter(pages);
font-size: 8pt;
color: #999;
}
}
Briefpapier-Elemente (konfigurierbar über Admin-Settings):
- Logo (PNG/SVG, Position: top-left oder top-center)
- Firmenname & Adresse (Header oder Footer)
- Kontaktdaten (Tel, Email, Website)
- Steuernummer / USt-IdNr. (Footer)
- Seitenzahlen (Footer rechts)
- Hintergrund-Wasserzeichen (optional, z.B. "ENTWURF")
- Bankverbindung (Footer)
Admin-Settings für Briefpapier:
- Endpoint:
PUT /api/settings/letterhead(admin only) - Felder: logo_path, company_name, street, zip_city, phone, email, website, tax_number, ust_id_nr, bank_name, bank_iban, bank_bic
- Briefpapier-Vorschau:
GET /api/settings/letterhead/preview→ PDF Preview
14.3 Dokument-Typen
| Dokument | Template | Briefpapier |
|---|---|---|
| Kaufvertrag (Sale Contract) | contract_template.html | Ja (Full Letterhead) |
| Rechnung (Invoice) | invoice_template.html | Ja (Full Letterhead) |
| DATEV-Export | – (CSV, kein PDF) | Nein |
| Fahrzeug-Inventarliste | inventory_template.html | Ja (Full Letterhead) |
ADR-009: WeasyPrint statt ReportLab
Status: Accepted
Entscheidung: WeasyPrint für alle PDF-Generierungen.
Begründung: HTML/CSS-Templates sind einfacher zu erstellen und zu warten
als ReportLab-Code. Briefpapier über CSS @page ist flexibler und
Web-Entwickler können Templates ohne Python-Kenntnisse anpassen.
Alternativen: ReportLab (pixel-präzise aber komplex), wkhtmltopdf (veraltet).
15. mobile.de Seller API – Detaillierte Integration
15.1 API-Übersicht
mobile.de bietet zwei Schnittstellen:
| Schnittstelle | Format | Use Case |
|---|---|---|
| Seller API (REST) | XML (application/vnd.de.mobile.seller-ad-v1.1+xml) |
Granulare CRUD-Operationen, Real-Time Sync |
| CSV Upload Interface | CSV (Semikolon-getrennt, ISO-8859-15) | Bulk-Upload aller Fahrzeuge auf einmal |
Entscheidung: Seller API (REST) als primäre Schnittstelle. CSV Upload als Fallback/Initial-Bulk-Import.
15.2 Authentifizierung
HTTP Basic Authentication (empfohlen):
- Username + Password aus mobile.de Seller Portal
Authorization: Basic <base64(username:password)>- Alternativ (deprecated):
X-MOBILE-SELLER-TOKENHeader
Credentials in Environment Variables:
MOBILEDE_SELLER_API_USERNAME=<username>
MOBILEDE_SELLER_API_PASSWORD=<password>
MOBILEDE_SELLER_KEY=<seller-key>
MOBILEDE_SELLER_API_URL=https://services.mobile.de
15.3 REST API Endpunkte
Base URL: https://services.mobile.de
| Method | Endpoint | Beschreibung |
|---|---|---|
| GET | /seller-api/sellers |
Alle Sellers abrufen |
| GET | /seller-api/sellers/{seller-key} |
Einzelnen Seller abrufen |
| GET | /seller-api/sellers/{seller-key}/ads |
Alle Anzeigen eines Sellers |
| POST | /seller-api/sellers/{seller-key}/ads |
Anzeige erstellen (XML Body) |
| GET | /seller-api/sellers/{seller-key}/ads/{adId} |
Einzelne Anzeige abrufen |
| PUT | /seller-api/sellers/{seller-key}/ads/{adId} |
Anzeige aktualisieren (XML Body) |
| DELETE | /seller-api/sellers/{seller-key}/ads/{adId} |
Anzeige löschen |
| POST | /seller-api/sellers/{seller-key}/ads/{adId}/vehicle-attribute/renewal-date |
Anzeige erneuern |
| PUT | /seller-api/sellers/{seller-key}/ads/{adId}/images |
Bilder hochladen/ändern |
| GET | /seller-api/sellers/{seller-key}/ads/{adId}/images |
Bild-URLs abrufen |
| DELETE | /seller-api/sellers/{seller-key}/ads/{adId}/images |
Alle Bilder löschen |
| GET | /seller-api/sellers/{seller-key}/ads/{ad-key}/statistic |
Anzeigen-Statistiken |
15.4 XML Ad-Format (Beispiel)
<ad:ad xmlns:ad="http://services.mobile.de/schema/ad">
<ad:vehicle>
<ad:category>Lkw</ad:category>
<ad:make>Mercedes-Benz</ad:make>
<ad:model>Actros</ad:model>
<ad:kilometre>150000</ad:kilometre>
<ad:firstRegistration>2020-03-15</ad:firstRegistration>
<ad:fuel>Diesel</ad:fuel>
<ad:gearbox>Manual</ad:gearbox>
<ad:price consumerPrice="true">45000.00</ad:price>
<ad:vat>1</ad:vat>
<ad:damagedVehicle>0</ad:damagedVehicle>
<ad:images>
<ad:image url="https://erp.media-on.de/files/vehicle-123/img1.jpg"/>
</ad:images>
</ad:vehicle>
</ad:ad>
15.5 Mapping: ERP Vehicle → mobile.de Ad
| ERP Feld | mobile.de XML Feld | Anmerkung |
|---|---|---|
| vehicle_type | ad:category | Lkw/Baumaschine/Pkw/Stapler/Transporter |
| brand | ad:make | |
| model | ad:model | |
| mileage_km | ad:kilometre | |
| first_registration | ad:firstRegistration | ISO Datum |
| power_kw | ad:power | kW-Wert |
| fuel_type | ad:fuel | Diesel/Petrol/Electric/Hybrid |
| sale_price | ad:price | consumerPrice="true" |
| registration_plate | ad:licensePlate | |
| vin | ad:vin | Fahrzeug-Identifikationsnummer |
| notes | ad:description | Freitext |
15.6 CSV Upload Interface (Fallback)
Format: Semikolon-getrennt, ISO-8859-15, .csv in .zip verpackt.
Pflichtfelder: internal number, category, make, model, kilometre, VAT,
damaged_vehicle, one-year-old car, new car, our recommendation, metallic, warranty.
Use Case: Initial-Bulk-Import aller Fahrzeuge nach mobile.de, oder wenn Seller API nicht verfügbar.
15.7 Rate-Limits
Die mobile.de Dokumentation nennt keine expliziten Rate-Limits. Dennoch implementieren wir einen eigenen Rate-Limiter (Redis Token Bucket):
- Max 10 Requests/Sekunde an mobile.de
- Retry-Queue bei HTTP 429 oder 5xx Fehlern (max 3 Retries mit Backoff)
ADR-010: Seller API (REST) statt CSV Upload als primäre Schnittstelle
Status: Accepted
Entscheidung: REST Seller API als primäre mobile.de Schnittstelle.
Begründung: Granulare CRUD-Operationen, Real-Time Sync, Bild-Management,
Statistiken. CSV nur für Initial-Import oder Bulk-Operationen.
Alternativen: CSV Upload nur (kein Real-Time, kein Bild-Management).
16. Buchhaltungsschnittstellen
16.1 DATEV-Export (primär)
Format: DATEV CSV (ASCII, semikolon-getrennt) – Buchungsstapel.
DATEV CSV Felder (Buchungsstapel-Format):
"Buchungsstapel";"01.01.2026";"31.01.2026";"0001";"Mandantenname";"1000";"Beraternummer"
"Umsatz";"Soll";"Haben";"Konto";"Gegenkonto";"Datum";"Belegfeld";"Buchungstext"
45000.00;"S";4200;0840;15.01.2026;"RE-2026-001";"Fahrzeugverkauf Mercedes Actros"
DATEV XML-Schnittstelle (erweitert):
- DATEV unterstützt zusätzlich XML-Format für Belegdaten
- Belegbilder + Belegdaten als DATEV-konforme XML
- Vorteil: Belegbilder (Rechnungen/Verträge) können direkt mitgeliefert werden
- Implementiert als Feature-Flag
datev_xml_export_enabled
16.2 Weitere Buchhaltungsschnittstellen
| System | Format | Status | Implementierung |
|---|---|---|---|
| DATEV | CSV (ASCII) + XML | Geplant (primär) | T06 – datev_service |
| Lexware | DATEV-CSV kompatibel | Über DATEV-Export nutzbar | Keine separate Implementierung nötig |
| SAP | CSV/IDoc | Zukunft (v2) | Feature-Flag, separates Export-Modul |
| DATEV Belegbilder | XML + Bilder | Zukunft (v2) | Feature-Flag datev_xml_export_enabled |
Lexware-Kompatibilität: Lexware unterstützt den DATEV-Import (CSV-Format). Daher ist der DATEV-CSV-Export automatisch Lexware-kompatibel. Lexware nutzt denselben DATEV-Datenexport "Belegbilder mit Belegdaten".
16.3 Export-Service Architektur
backend/app/utils/datev.py # DATEV CSV Formatter
backend/app/utils/accounting_export.py # Generic Export Base (für zukünftige Formate)
backend/app/services/datev_service.py # DATEV Export Logic
Generic Export Pattern (für zukünftige SAP/Weitere):
class AccountingExporter(ABC):
@abstractmethod
def export(self, sales: list[Sale], period: DateRange) -> bytes: ...
class DatevCSVExporter(AccountingExporter): ...
class DatevXMLExporter(AccountingExporter): ... # v2
class SAPExporter(AccountingExporter): ... # v2
ADR-011: DATEV CSV als primäre Buchhaltungsschnittstelle
Status: Accepted
Entscheidung: DATEV CSV-Format als primärer Export. Generic Export Base
für zukünftige Formate (XML, SAP).
Begründung: DATEV ist der deutsche Standard, Lexware-kompatibel,
einfach zu generieren. Abstract Base Class ermöglicht Erweiterung ohne
Code-Duplikation.
Alternativen: Direkte DATEV online API (zu komplex, benötigt
Zertifizierung), SAP IDoc (overkill für ~10 Nutzer).
17. KI-Copilot: Vollständige Systemsteuerung
17.1 Anforderung
Der KI-Copilot soll ALLE System-Funktionen steuern können. Der User kann per Text oder Sprache mit dem Copilot interagieren und beliebige Aktionen ausführen lassen.
17.2 Technische Umsetzung: Function Calling
Mechanismus: OpenRouter Function Calling / Tool Use
Der LLM (Claude/GPT-4) erhält einen System-Prompt mit allen verfügbaren Functions (Tools). Bei einer User-Anfrage entscheidet der LLM, welche Function aufgerufen werden muss, und gibt strukturiertes JSON zurück.
User: "Erstelle ein neues Fahrzeug: Mercedes Actros, 150000 km, 45000 EUR"
↓
LLM Output: {
"function": "create_vehicle",
"arguments": {
"vehicle_type": "lkw",
"brand": "Mercedes-Benz",
"model": "Actros",
"mileage_km": 150000,
"purchase_price": 45000.00
}
}
↓
Copilot Service: POST /api/vehicles (intern, mit User-JWT)
↓
Response to User: "Fahrzeug erstellt ✓ (ID: abc-123). Soll ich es
nach mobile.de pushen?"
17.3 Verfügbare System-Functions (Tools)
| Function | Beschreibung | Endpoint |
|---|---|---|
| search_vehicles | Fahrzeuge suchen/filtern | GET /api/vehicles |
| create_vehicle | Neues Fahrzeug anlegen | POST /api/vehicles |
| update_vehicle | Fahrzeug aktualisieren | PUT /api/vehicles/:id |
| delete_vehicle | Fahrzeug löschen (soft) | DELETE /api/vehicles/:id |
| push_to_mobilede | Nach mobile.de pushen | POST /api/vehicles/:id/mobilede-push |
| upload_ocr | OCR-Scan hochladen | POST /api/ocr/upload |
| apply_ocr | OCR-Daten anwenden | POST /api/ocr/results/:id/apply |
| search_contacts | Kontakte suchen | GET /api/contacts |
| create_contact | Kontakt anlegen | POST /api/contacts |
| update_contact | Kontakt aktualisieren | PUT /api/contacts/:id |
| upload_file | Datei hochladen | POST /api/vehicles/:id/files |
| list_files | Dateien auflisten | GET /api/vehicles/:id/files |
| create_sale | Verkauf anlegen | POST /api/sales |
| get_sale | Verkaufsdetails | GET /api/sales/:id |
| generate_contract | Vertrag generieren | POST /api/sales/:id/contract |
| export_datev | DATEV-Export | POST /api/datev/export |
| retouch_image | Bild retuschieren | POST /api/retouch/process |
| price_compare | Preisvergleich | POST /api/retouch/price-compare |
| get_statistics | Dashboard-Statistiken | GET /api/statistics |
| search_copilot | Eigene Historie durchsuchen | GET /api/copilot/history |
Echt ALLES – inkl. Admin-Functions (nur für Admin-Role):
| Function | Beschreibung | Endpoint |
|---|---|---|
| list_users | User auflisten | GET /api/users |
| create_user | User anlegen | POST /api/users |
| deactivate_user | User deaktivieren | DELETE /api/users/:id |
| update_letterhead | Briefpapier ändern | PUT /api/settings/letterhead |
17.4 Safety & Confirmation
Action Preview Pattern (wie in T07 definiert):
- LLM schlägt Action vor (Function + Arguments)
- Frontend zeigt Action Preview: "Ich möchte folgendes tun: [Action]"
- User bestätigt oder ablehnt
- Bei Bestätigung: Copilot führt Action aus (interner API-Call mit User-JWT)
- Ergebnis wird im Chat angezeigt
Auto-Execute bei Read-Only Actions:
- search_vehicles, search_contacts, get_sale, list_files, get_statistics, price_compare → automatisch ausgeführt (kein Bestätigung nötig)
- Alle schreibenden Actions → User-Bestätigung erforderlich
RBAC durchgesetzt: Copilot nutzt den JWT des aktuellen Users. Ein Verkäufer kann keine User verwalten, auch nicht über den Copilot.
17.5 System-Prompt Struktur
Du bist der KI-Assistent für ein Nutzfahrzeug-ERP-System.
Du kannst Fahrzeuge verwalten, Kontakte anlegen, Verkäufe erstellen,
OCR-Scans verarbeiten, Bilder retuschieren und DATEV-Exporte erstellen.
Verfügbare Aktionen:
[Liste aller Functions mit Beschreibung und Parameters]
Regeln:
- Bei schreibenden Aktionen: schlage die Aktion vor und warte auf Bestätigung
- Bei Lese-Aktionen: führe sie direkt aus und präsentiere die Ergebnisse
- Antworte auf Deutsch (oder Englisch je nach User-Setting)
- Wenn Informationen fehlen: frage nach
ADR-012: Function Calling für KI-Vollsteuerung
Status: Accepted
Entscheidung: OpenRouter Function Calling für alle System-Aktionen.
Begründung: Strukturierte JSON-Ausgabe, zuverlässig, von Claude/GPT-4
unterstützt. User-Bestätigung für schreibende Aktionen als Safety-Mechanismus.
Alternativen: Free-text parsing (fehleranfällig), MCP Protocol
(zu neu, noch nicht etabliert).
18. KI Multi-Provider Support
18.1 OpenRouter als Unified Gateway
Entscheidung: OpenRouter als alleiniges KI-Gateway. Keine direkten Provider-Anbindungen.
Begründung: OpenRouter bietet:
- 400+ Modelle von allen major providers (OpenAI, Anthropic, Google, Meta, Mistral, etc.)
- Ein API-Key für alle Modelle (kein separates Key-Management)
- OpenAI-kompatible API (drop-in replacement)
- Provider-Routing: Automatic fallback auf Backup-Provider bei Ausfall
- Side-by-side Pricing: Kostenvergleich aller Modelle
- Tool Calling Support: Modelle mit Function Calling verfügbar
18.2 Modell-Konfiguration pro Use-Case
Der User kann in den Admin-Settings konfigurieren, welches Modell für welchen Use-Case verwendet wird:
| Use Case | Default Modell | Alternative |
|---|---|---|
| OCR (ZB I/II) | qwen/qwen-2.5-vl-72b-instruct | openai/gpt-4o |
| Bild-Retusche | black-forest-labs/flux-1.1-pro | stabilityai/stable-diffusion-xl |
| Copilot (Chat) | anthropic/claude-3.5-sonnet | openai/gpt-4o, google/gemini-flash-1.5 |
| Copilot (Voice STT) | openai/whisper-large-v3 | – |
Settings Endpoint:
GET /api/settings/ai-models → Aktuelle Modell-Konfiguration
PUT /api/settings/ai-models → Modell-Konfiguration aktualisieren (admin only)
Config in DB (Settings-Tabelle):
{
"ocr_model": "qwen/qwen-2.5-vl-72b-instruct",
"retouch_model": "black-forest-labs/flux-1.1-pro",
"copilot_chat_model": "anthropic/claude-3.5-sonnet",
"copilot_voice_model": "openai/whisper-large-v3"
}
18.3 Provider-Fallback
OpenRouter bietet automatisches Provider-Routing:
- Bei Ausfall des primären Providers → automatischer Fallback auf Backup-Provider
- Beispiel: anthropic/claude-3.5-sonnet → bei Anthropic-Ausfall → OpenRouter versucht alternative Provider für dasselbe Modell
Eigenes Fallback-Handling (zusätzlich):
COPILOT_MODELS = [
"anthropic/claude-3.5-sonnet",
"openai/gpt-4o",
"google/gemini-flash-1.5",
"meta-llama/llama-3.1-70b-instruct"
]
# Bei Fehler: nächstes Modell in der Liste probieren
18.4 Kostenkontrolle
- OpenRouter zeigt Token-Kosten pro Request
- Logging: ai_usage_log Tabelle (model, tokens_in, tokens_out, cost, user_id, timestamp)
- Rate-Limiting pro User (Redis Token Bucket)
- Monatsbudget konfigurierbar (Admin-Setting ai_monthly_budget_eur)
- Bei Überschreitung: Warnung + ggf. Drosselung
ADR-013: OpenRouter als alleiniges KI-Gateway
Status: Accepted
Entscheidung: Alle KI-Aufrufe über OpenRouter, keine direkten Provider-APIs.
Begründung: Ein API-Key, 400+ Modelle, Provider-Routing, einheitliche API,
keine Multi-SDK-Wartung. Modell-Konfiguration pro Use-Case ermöglicht
flexible Anpassung ohne Code-Änderung.
Alternativen: Direkte OpenAI + Anthropic APIs (Multi-Key-Management,
höhere Wartung), lokale Modelle via Ollama (zu langsam für OCR/Retusche,
aber möglich als Fallback für Copilot-Chat).
18.5 Optional: Lokale Modelle via Ollama (Copilot Fallback)
Für den Copilot-Chat (nicht OCR/Retusche) kann optional ein lokales Modell via Ollama als Offline-Fallback konfiguriert werden:
- Feature-Flag local_model_fallback_enabled
- Ollama läuft als 5. Container (optional, nur bei Bedarf)
- Modell: llama3.1:8b-instruct oder qwen2.5:14b
- Use Case: Offline-Betrieb oder Kostenersparnis für einfache Chat-Tasks
- Function Calling Support: Llama 3.1 unterstützt native Tool-Calling
Architektur-Erweiterung (v2, Feature-Flag):
Copilot Request → Try OpenRouter → Fail → Try local Ollama → Fail → Error
13. Konventionen
13.1 Backend
- async/await für alle DB-Operationen und externen API-Calls
- Pydantic v2 für alle Request/Response-Schemas
- SQLAlchemy 2.0 mit typed Mapped columns
- Alembic für DB-Migrationen (kein auto-create in Produktion)
- Router-Prefix:
/api/<resource> - Error-Format:
{"error": {"code": "VEHICLE_NOT_FOUND", "message": "...", "details": {}}}
13.2 Frontend
- TypeScript strict mode
- Tailwind CSS für Styling (kein CSS-in-JS)
- Server Components für statische Daten, Client Components für Interaktion
- API-Client: zentrale fetch-Wrapper mit Auth-Header-Injection
- Date-Naming: kebab-case für Dateien, camelCase für Variablen
13.3 Git
- Conventional Commits:
feat:,fix:,docs:,test:,refactor: - Branch-Naming:
feature/T01-vehicle-module,fix/...,hotfix/... - PRs erforderlich für main-Branch
19. Provider Architecture (Multi-Provider mit Ollama Cloud)
19.1 Provider Abstraktion (ABC Pattern)
from abc import ABC, abstractmethod
class BaseAIProvider(ABC):
@abstractmethod
async def chat(self, messages, tools=None, model=None) -> dict: ...
@abstractmethod
async def vision(self, image_base64, prompt, model=None) -> str: ...
@abstractmethod
async def image_edit(self, image_base64, instruction, model=None) -> str: ...
@abstractmethod
def list_models(self) -> list[str]: ...
@abstractmethod
def health_check(self) -> bool: ...
class OpenRouterProvider(BaseAIProvider): ...
class OllamaCloudProvider(BaseAIProvider): ...
class OllamaLocalProvider(BaseAIProvider): ...
Neue Provider durch Implementierung von BaseAIProvider + Eintrag in providers.yaml hinzufügbar.
19.2 providers.yaml
providers:
openrouter:
enabled: true
api_base: https://openrouter.ai/api/v1
api_key: ${OPENROUTER_API_KEY}
models: [anthropic/claude-3.5-sonnet, openai/gpt-4o, qwen/qwen2.5-vl]
ollama_cloud:
enabled: true
api_base: ${OLLAMA_CLOUD_URL}
api_key: ${OLLAMA_CLOUD_API_KEY}
models: [glm-5.2:cloud, qwen2.5:14b, llava:13b]
ollama_local:
enabled: false
api_base: http://ollama:11434
models: [llama3.1:8b-instruct, qwen2.5:14b]
19.3 Use-Case → Provider Mapping
| Use Case | Default | Fallback |
|---|---|---|
| Copilot Chat | ollama_cloud (glm-5.2) | openrouter (claude-3.5) |
| OCR | openrouter (qwen2.5-vl) | ollama_cloud (llava) |
| Bildretusche | openrouter (flux.1-pro) | - |
| Voice | openrouter | browser-native |
Admin konfigurierbar via PUT /api/settings/ai-providers
ADR-014: Multi-Provider mit Ollama Cloud
- Ollama Cloud als ECHTER Provider (nicht nur Fallback)
- Provider-Abstraktion ermöglicht beliebige zukünftige Provider
- Admin kann pro Use-Case Provider wählen