# 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-Persistenz - `redis_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 | | email | 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) | | | email | 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 ```bash # 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 | `` 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**: ```css @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 ` - Alternativ (deprecated): `X-MOBILE-SELLER-TOKEN` Header **Credentials in Environment Variables**: ``` MOBILEDE_SELLER_API_USERNAME= MOBILEDE_SELLER_API_PASSWORD= MOBILEDE_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) ```xml Lkw Mercedes-Benz Actros 150000 2020-03-15 Diesel Manual 45000.00 1 0 ``` ### 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): ```csv "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): ```python 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): 1. LLM schlägt Action vor (Function + Arguments) 2. Frontend zeigt Action Preview: "Ich möchte folgendes tun: [Action]" 3. User bestätigt oder ablehnt 4. Bei Bestätigung: Copilot führt Action aus (interner API-Call mit User-JWT) 5. 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): ```json { "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): ```python 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/` - 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) ```python 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 ```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