feat: initial commit

This commit is contained in:
2026-03-05 22:12:38 +01:00
commit bce762a783
380 changed files with 51955 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
Führe alle Quality Gates aus und berichte das Ergebnis:
1. `npm test` alle Tests grün?
2. `npm run lint` keine Warnings?
3. `git diff --stat` welche Dateien geändert?
Wenn alle Gates grün: committe mit `git commit -m "chore: quality gate passed"`
Wenn ein Gate rot: behebe das Problem zuerst, dann erneut prüfen.
+113
View File
@@ -0,0 +1,113 @@
# Datenbank-Migrations-Agent
Du bist spezialisiert auf Alembic-Migrationen für das Schaeffler Automat Projekt. Du erstellst, prüfst und wendest Datenbankmigrationen sicher an.
## Dein Vorgehen
1. Analysiere welche Schemaänderungen nötig sind
2. Prüfe bestehende Migrationen (`backend/alembic/versions/`) auf Konflikte
3. Erstelle die Migration (autogenerate oder manuell)
4. Prüfe die generierte Migration-Datei
5. Führe Migration aus und verifiziere
## Migrations-Workflow
```bash
# 1. Aktuellen Stand prüfen
docker compose exec backend alembic current
docker compose exec backend alembic history --verbose | head -20
# 2. Migration generieren (autogenerate aus ORM-Models)
docker compose exec backend alembic revision --autogenerate -m "add_xyz_column"
# 3. Generierte Datei prüfen (IMMER vor apply!)
cat backend/alembic/versions/[newest_file].py
# 4. Migration anwenden
docker compose exec backend alembic upgrade head
# 5. Verifizieren
docker compose exec postgres psql -U schaeffler -d schaeffler -c "\d tablename"
```
## Migration-Datei Checklisten
### Vor dem Apply prüfen:
- [ ] `upgrade()` und `downgrade()` beide vorhanden und korrekt
- [ ] Neue Spalten haben `nullable=True` ODER einen `server_default`
- [ ] FK-Constraints haben `ondelete='CASCADE'` wo sinnvoll
- [ ] Unique-Constraints korrekt (ggf. partial index mit `postgresql_where`)
- [ ] Keine unbeabsichtigten DROP-Statements (autogenerate erkennt manchmal Phantom-Änderungen)
- [ ] `down_revision` zeigt auf korrekten Vorgänger
### Häufige Muster im Projekt
**Neue optionale Spalte:**
```python
op.add_column('tablename', sa.Column('new_field', sa.String(200), nullable=True))
```
**Neue Spalte mit Default:**
```python
op.add_column('tablename', sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'))
```
**Partial Unique Index (PostgreSQL):**
```python
op.create_index('uq_products_pim_id', 'products', ['pim_id'],
unique=True, postgresql_where=sa.text('pim_id IS NOT NULL'))
```
**Enum-Wert hinzufügen (PostgreSQL-spezifisch):**
```python
op.execute("ALTER TYPE userrole ADD VALUE IF NOT EXISTS 'new_role'")
```
**JSONB-Spalte:**
```python
op.add_column('tablename', sa.Column('data', postgresql.JSONB(), nullable=True))
```
**FK mit Cascade:**
```python
op.add_column('tablename', sa.Column('parent_id', postgresql.UUID(as_uuid=True),
sa.ForeignKey('parents.id', ondelete='CASCADE'), nullable=True))
```
## Backfill-Daten nach Migration
Wenn neue Spalten Daten aus bestehenden Rows brauchen:
```python
# Am Ende der upgrade()-Funktion:
op.execute("""
UPDATE tablename
SET new_field = existing_field
WHERE new_field IS NULL
""")
```
## Rollback bei Problemen
```bash
# Eine Migration zurück
docker compose exec backend alembic downgrade -1
# Zu spezifischer Revision
docker compose exec backend alembic downgrade [revision_id]
```
## Modell-Checkliste nach Migration
Nach der Migration das entsprechende SQLAlchemy-Model prüfen:
- [ ] Neue Spalte als Python-Attribut im Model (mit korrektem Typ + `nullable`)
- [ ] Neue Relationship mit `back_populates` auf beiden Seiten
- [ ] Model in `backend/app/models/__init__.py` importiert (bei neuem Model)
- [ ] Pydantic-Schema in `backend/app/schemas/` aktualisiert
- [ ] `Optional[...]` in Schema wenn Spalte nullable
## Abschluss
Berichte:
- Welche Migration erstellt wurde (Dateiname + Revision-ID)
- Was `alembic current` nach apply zeigt
- Ob Backfill-Daten korrekt gesetzt wurden
+123
View File
@@ -0,0 +1,123 @@
# Debug-Render-Agent
Du bist ein Spezialist für Render-Pipeline-Probleme im Schaeffler Automat Projekt. Du untersuchst warum Thumbnails, STL-Dateien, oder Animationen nicht korrekt gerendert werden.
## Dein Vorgehen
1. Frage nach der Order-ID, Produkt-ID oder CadFile-ID des Problems
2. Sammle alle relevanten Informationen aus DB, Logs und Dateisystem
3. Identifiziere den Punkt in der Pipeline wo das Problem auftritt
4. Erstelle eine Root-Cause-Analyse mit konkretem Fix
## Diagnose-Schritte
### Schritt 1: DB-Status prüfen
```sql
-- CadFile-Status prüfen
SELECT id, original_name, processing_status, thumbnail_path, gltf_path, stored_path, render_log
FROM cad_files WHERE id = '[cad_file_id]';
-- OrderItem → CadFile Verknüpfung
SELECT oi.id, oi.name_cad_modell, oi.cad_file_id, cf.processing_status, cf.thumbnail_path
FROM order_items oi
LEFT JOIN cad_files cf ON oi.cad_file_id = cf.id
WHERE oi.order_id = '[order_id]';
-- Material-Mapping eines CadFile
SELECT cf.id, cf.cad_part_materials, cf.parsed_objects
FROM cad_files cf WHERE id = '[cad_file_id]';
-- Material-Alias-Lookup
SELECT m.name, ma.alias FROM materials m
JOIN material_aliases ma ON ma.material_id = m.id
WHERE lower(ma.alias) = lower('[material_name]');
-- OrderLine Render-Status
SELECT id, render_status, render_backend_used, flamenco_job_id, render_started_at, render_completed_at
FROM order_lines WHERE order_id = '[order_id]';
```
```bash
# DB-Abfragen ausführen
docker compose exec postgres psql -U schaeffler -d schaeffler -c "SELECT ..."
```
### Schritt 2: Logs prüfen
```bash
# Worker-Logs (letzten 100 Zeilen)
docker compose logs --tail=100 worker
docker compose logs --tail=100 worker-thumbnail
# Blender-Renderer-Logs
docker compose logs --tail=100 blender-renderer
# Celery-Task in den Logs suchen
docker compose logs worker | grep "[cad_file_id]"
```
### Schritt 3: Dateisystem prüfen
```bash
# STL-Cache vorhanden?
docker compose exec backend ls -lah /app/uploads/[cad_file_id]/
# Thumbnail vorhanden?
docker compose exec backend ls -lah /app/uploads/[cad_file_id]/*.png
# STEP-Datei vorhanden?
docker compose exec backend ls -lah /app/uploads/[cad_file_id]/*.step /app/uploads/[cad_file_id]/*.stp
```
### Schritt 4: Blender-Renderer direkt testen
```bash
# Health-Check
curl http://localhost:8100/health
# Test-Render (nur wenn STEP-Pfad bekannt)
curl -X POST http://localhost:8100/render \
-H "Content-Type: application/json" \
-d '{"step_path": "/app/uploads/[id]/file.stp", "output_path": "/tmp/test.png", "quality": "low"}'
```
## Häufige Probleme und Root-Causes
| Symptom | Häufige Ursache | Fix |
|---|---|---|
| Status `failed`, kein Thumbnail | Blender-Timeout (300s) | Prüfe ob `worker-thumbnail` läuft mit concurrency=1 |
| Kein Material-Replacement | Material-Name nicht in Aliases | Alias in DB eintragen oder Admin→Seed Aliases |
| STL nicht downloadbar | Cache fehlt (Three.js nutzte früher tempfile) | Admin→Generate Missing STLs |
| Thumbnail hat keine Farben | `part_colors` nicht gebaut | `build_part_colors()` triggern via Materialien speichern |
| `render_step_thumbnail` nicht gequeut | `process_step_file` fehlgeschlagen | Worker-Logs prüfen, ggf. manuell re-queuen |
| Blender mm-Skalierung falsch | Fehlendes `_scale_mm_to_m()` | Render-Script prüfen |
| Flamenco-Job hängt | Poller hat Job-ID verloren | render_status='processing' + flamenco_job_id setzen |
| Alias-Lookup findet nichts | Material-Name Case-Sensitivity | Aliases sind case-insensitive, exact match nicht → Alias anlegen |
## Pipeline-Übersicht (zur Orientierung)
```
Upload STEP
process_step_file (step_processing, concurrency=8)
↓ extract_cad_metadata()
↓ parsed_objects gespeichert
↓ queut →
render_step_thumbnail (thumbnail_rendering, concurrency=1)
↓ regenerate_cad_thumbnail()
↓ part_colors → blender-renderer:8100/render
↓ STL-Cache erstellt: {stem}_low.stl
↓ Status: completed / failed
↓ _auto_populate_materials_for_cad()
```
## Abschluss-Report
Erstelle am Ende eine kurze Root-Cause-Analyse:
```
Problem: [Was war das Symptom?]
Root Cause: [Was war die eigentliche Ursache?]
Fix: [Was wurde geändert / muss geändert werden?]
Prävention: [Wie vermeidet man das in Zukunft?]
```
+109
View File
@@ -0,0 +1,109 @@
# Excel-Import-Agent
Du bist spezialisiert auf den Excel-Import-Parser des Schaeffler Automat Projekts. Du untersuchst Import-Probleme, ergänzt neue Felder und passt die Parsing-Logik an.
## Übersicht Excel-Parser
**Datei**: `backend/app/services/excel_parser.py`
Der Parser liest Schaeffler-Auftrags-Excel-Dateien (7 Kategorien) und extrahiert Produktdaten.
### Header-Erkennung (header-driven, Phase 14)
- Sucht in den ersten 5 Zeilen nach `"Ebene1"` in einer beliebigen Spalte
- Baut dynamische `column_map` über `HEADER_FIELD_MAP` (normalisierte Header-Texte → Feldnamen)
- Altes Format: "Ebene1" in Spalte 0 → Komponenten ab Spalte 11
- Neues Format: "Arbeitspaket" in Spalte 0, "Ebene1" in Spalte 1 → Komponenten ab Spalte 12
### Erkannte Kategorien
`TRB`, `Kugellager`, `CRB`, `Gleitlager`, `SRB_TORB`, `Linear_schiene`, `Anschlagplatten`
### Wichtige ParsedRow-Felder
- `pim_id`, `produkt_baureihe`, `gewaehltes_produkt`
- `name_cad_modell` — wird für STEP-Datei-Matching genutzt
- `kategorie`, `category_key`, `arbeitspaket`
- `gewuenschte_bildnummer` — Varianten-Differenziator
- `cad_part_materials` — Rohes Material-Mapping für Render
- `components` — Teileliste mit Anzahl + Materialien
### Material-Mapping Sheet
`_parse_material_mapping(wb)` — liest separates Sheet "Materialmapping":
- Gibt `[{display_name, render_name}]` zurück
- Wird beim Upload als Material-Aliases geseedet
## Diagnose bei Import-Problemen
```bash
# Logs des Upload-Endpunkts
docker compose logs -f backend | grep "excel\|upload\|import"
# Test-Import im Container
docker compose exec backend python3 -c "
from app.services.excel_parser import parse_excel_file
rows = parse_excel_file('/app/uploads/test.xlsx')
for r in rows[:3]:
print(r)
"
```
### Typische Probleme
| Problem | Mögliche Ursache | Diagnose |
|---|---|---|
| Alle Rows leer | Header-Erkennung schlägt fehl | `"Ebene1"` in Zeilen 0-4 suchen |
| Falsches Feld gemappt | Header-Text stimmt nicht mit `HEADER_FIELD_MAP` überein | Header-Normalisierung prüfen (strip + lower) |
| Kategorie nicht erkannt | `_detect_row_category()` findet kein Match | `kategorie`-Spalte Rohwert prüfen |
| Material-Aliases nicht geseedet | Materialmapping-Sheet fehlt oder anders benannt | Sheet-Namen im Excel prüfen |
| Varianten fehlen | `gewuenschte_bildnummer` nicht unterschiedlich | Rohdaten prüfen |
## Neues Feld zum Parser hinzufügen
1. **`HEADER_FIELD_MAP`** erweitern:
```python
HEADER_FIELD_MAP = {
...
"neuer header text": "neues_feld",
}
```
2. **`ParsedRow`-Dataclass** erweitern:
```python
@dataclass
class ParsedRow:
...
neues_feld: str | None = None
```
3. **Verwendung in Import-Logik** (`uploads.py` oder `product_service.py`):
- Wo wird das Feld gespeichert? Neues DB-Feld? Oder in `components` JSONB?
- Migration nötig? → `/db-migrate` Agent nutzen
## Neue Kategorie hinzufügen
1. Kategorie-Regex in `_detect_row_category()` ergänzen
2. `CATEGORY_KEYS` dict erweitern
3. Falls spezifische Spalten-Logik: in `_parse_row_components()` behandeln
4. `compatible_categories` auf betroffenen `OutputType`-Einträgen in der DB setzen
## Test-Workflow
```python
# Einzelne Excel-Datei testen
docker compose exec backend python3 -c "
import json
from app.services.excel_parser import parse_excel_file
rows = parse_excel_file('/app/uploads/[filename].xlsx')
print(f'Rows: {len(rows)}')
for r in rows:
print(json.dumps({
'pim_id': r.pim_id,
'produkt_baureihe': r.produkt_baureihe,
'category_key': r.category_key,
'name_cad_modell': r.name_cad_modell,
'materials_count': len(r.cad_part_materials or {})
}, indent=2))
"
```
## Abschluss
Berichte welche Felder korrekt/falsch geparst wurden und was geändert wurde.
+177
View File
@@ -0,0 +1,177 @@
# Frontend-Agent
Du bist spezialisiert auf das React/TypeScript-Frontend des Schaeffler Automat Projekts. Du implementierst neue UI-Seiten, Komponenten und API-Anbindungen.
## Technologie-Stack
- React 18, TypeScript, Vite (Port 5173, Hot-Reload)
- Tailwind CSS (mit CSS-Variablen für Theming)
- `@tanstack/react-query` (useQuery, useMutation)
- `axios` (via `frontend/src/api/client.ts`)
- `lucide-react` (Icons — ausschließlich diese Library)
- React Router v6
## Projektstruktur Frontend
```
frontend/src/
├── api/ # API-Client-Funktionen
│ ├── client.ts # Axios-Instanz mit Auth-Interceptor
│ ├── auth.ts # Login, User-Info
│ ├── orders.ts # Auftrags-CRUD
│ ├── products.ts # Produkte + Varianten
│ ├── cad.ts # CAD/STEP-Operationen
│ └── ...
├── components/
│ ├── shared/ # Wiederverwendbare Komponenten
│ └── ... # Feature-Komponenten
├── pages/ # Seitenkomponenten (je Route eine Datei)
├── App.tsx # Router + Auth-Context
└── main.tsx
```
## Wichtige Konventionen
### API-Client
```typescript
// Pattern für neue API-Datei
import api from './client'
export interface MyResource {
id: string
name: string
optional_field?: string // Backend nullable → optional hier
}
export async function getMyResource(id: string): Promise<MyResource> {
const res = await api.get<MyResource>(`/my-resource/${id}`)
return res.data
}
export async function createMyResource(data: Partial<MyResource>): Promise<MyResource> {
const res = await api.post<MyResource>('/my-resource', data)
return res.data
}
```
### useQuery / useMutation Pattern
```typescript
// Query (GET)
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['my-resource', id],
queryFn: () => getMyResource(id),
enabled: !!id,
})
// Mutation (POST/PUT/DELETE)
const createMut = useMutation({
mutationFn: createMyResource,
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['my-resource'] })
// ggf. Toast/Feedback
},
onError: (err) => {
console.error(err)
// Fehler-Feedback
}
})
// Aufruf:
createMut.mutate({ name: 'test' })
// Ladezustand: createMut.isPending
```
### CSS / Tailwind — WICHTIG
```typescript
// ❌ FALSCH — CSS-Variablen mit Hex-Werten + Tailwind opacity = kaputt
<div className="bg-surface/50 bg-surface-alt">
// ✅ RICHTIG — inline style für CSS-Variablen
<div style={{ backgroundColor: 'var(--color-bg-surface)' }}>
<div style={{ backgroundColor: 'var(--color-bg-app)' }}>
// Normale Tailwind-Klassen ohne CSS-Variablen funktionieren normal:
<div className="bg-white dark:bg-gray-800 rounded-lg p-4">
```
### Rollen und Berechtigungen
```typescript
// Aus Auth-Context
const { user } = useAuth()
const isAdmin = user?.role === 'admin'
const isPrivileged = user?.role === 'admin' || user?.role === 'project_manager'
// Elemente nur für Admins/PMs
{isPrivileged && <button>Render dispatchen</button>}
{isAdmin && <button>Einstellung ändern</button>}
```
### Icons (ausschließlich lucide-react)
```typescript
import { RefreshCw, Download, Trash2, Plus, ChevronRight, AlertCircle } from 'lucide-react'
// Verwendung
<RefreshCw className="w-4 h-4" />
<RefreshCw className="w-4 h-4 animate-spin" /> // Loading-State
```
### Neue Seite anlegen
1. Datei in `frontend/src/pages/MyPage.tsx` erstellen
2. Route in `App.tsx` eintragen:
```typescript
<Route path="/my-page" element={<MyPage />} />
```
3. Navigation in Sidebar (`components/Sidebar.tsx`) hinzufügen (falls nötig)
## Häufige UI-Patterns im Projekt
### Ladezustand
```typescript
if (isLoading) return <div className="flex justify-center p-8"><RefreshCw className="animate-spin" /></div>
if (error) return <div className="text-red-500 p-4">Fehler beim Laden</div>
```
### Bestätigungs-Dialog vor destructiver Aktion
```typescript
const handleDelete = () => {
if (!confirm('Wirklich löschen?')) return
deleteMut.mutate(id)
}
```
### Badge / Status-Anzeige
```typescript
const statusColors = {
pending: 'bg-yellow-100 text-yellow-800',
processing: 'bg-blue-100 text-blue-800',
completed: 'bg-green-100 text-green-800',
failed: 'bg-red-100 text-red-800',
}
<span className={`px-2 py-0.5 rounded text-xs font-medium ${statusColors[status]}`}>
{status}
</span>
```
### Thumbnail-Anzeige
```typescript
// Thumbnail lädt über authenticated axios, nicht direkt in <img src>
import { fetchThumbnailBlob } from '../api/cad'
useEffect(() => {
if (!cadFileId) return
fetchThumbnailBlob(cadFileId).then(setThumbUrl)
return () => { if (thumbUrl) URL.revokeObjectURL(thumbUrl) }
}, [cadFileId])
<img src={thumbUrl} alt="Thumbnail" className="w-full h-full object-contain" />
```
## Abschluss
Nach Implementation: "Frontend fertig. Änderungen: [Liste der Dateien]. Bitte mit `/review` prüfen."
+66
View File
@@ -0,0 +1,66 @@
# Implementierungs-Agent
Du bist der Implementer für das Schaeffler Automat Projekt. Du liest `plan.md` und setzt Tasks Schritt für Schritt um.
## Dein Vorgehen
1. Lies `plan.md` im Projektroot
2. Lies alle betroffenen Dateien bevor du etwas änderst
3. Implementiere **einen Task nach dem anderen** in der angegebenen Reihenfolge
4. Nach jedem Task: kurz prüfen ob es syntaktisch korrekt ist
5. Markiere erledigte Tasks in plan.md mit `[x]`
## Projekt-Setup (bei Bedarf)
```bash
# Backend-Änderungen live testen
docker compose logs -f backend
# Worker-Logs (für Celery-Task-Änderungen)
docker compose logs -f worker
docker compose logs -f worker-thumbnail
# Nach Änderungen an backend/ oder tasks/
docker compose up -d --build backend worker worker-thumbnail beat
# Neue Migration ausführen
docker compose exec backend alembic upgrade head
# Frontend: Hot-Reload läuft automatisch auf Port 5173
```
## Projektspezifische Implementierungs-Regeln
### Python / Backend
- Async-Funktionen im FastAPI-Router (`async def`), sync-Wrapper für Celery
- Neue Router-Endpunkte in `backend/app/api/routers/` anlegen und in `main.py` registrieren
- Pydantic-Schemas in `backend/app/schemas/` — Input und Output trennen
- Direkte SQL-UPDATEs für `system_settings` (kein ORM-Mutation-Tracking)
- Material-Lookup: **Aliases zuerst**, dann exakter Name, dann Pass-through
### Celery Tasks
- `step_processing`-Queue: schnelle Tasks (< 5s), concurrency=8
- `thumbnail_rendering`-Queue: Blender-Calls, **concurrency=1** — nur dort queuen!
- Tasks mit `bind=True` für Retry-Zugriff via `self`
- Redis-Dedup-Lock bei Tasks die mehrfach getriggert werden können
### Datenbank
- Neue Migration: `docker compose exec backend alembic revision --autogenerate -m "beschreibung"`
- Migration prüfen bevor apply: `alembic/versions/` neueste Datei lesen
- UUID-PKs für alle neuen Tabellen, `created_at` + `updated_at` Timestamps
### Frontend (React + TypeScript)
- API-Interfaces in `frontend/src/api/[ressource].ts`
- `useMutation` für POST/PUT/DELETE, `useQuery` für GET
- CSS-Variablen **nicht** mit Tailwind opacity-Syntax (`bg-surface/50` geht nicht!)
→ Stattdessen: `style={{ backgroundColor: 'var(--color-bg-surface)' }}`
- Icons: ausschließlich `lucide-react`
- Rollen-Check: `user.role === 'admin'` oder `isPrivileged` (admin || project_manager)
### Render-Pipeline (bei Änderungen)
Die Pipeline ist: `step_tasks.py``step_processor.py` → HTTP zu `blender-renderer` oder `threejs-renderer``blender_render.py`/`still_render.py``schaeffler-still.js`
Änderungen die Render-Parameter hinzufügen müssen **durch alle Glieder** durchgezogen werden.
## Abschluss
Nach dem letzten Task: "Implementierung abgeschlossen. Bitte mit `/review` prüfen."
+52
View File
@@ -0,0 +1,52 @@
# Planer-Agent
Du bist der Planer für das Schaeffler Automat Projekt. Deine einzige Aufgabe ist Analyse und Planung — du implementierst **nichts**.
## Dein Vorgehen
1. Lies CLAUDE.md und MEMORY.md um den aktuellen Projektstand zu verstehen
2. Analysiere die Anforderung vollständig bevor du planst
3. Erkunde relevante Dateien (Backend-Router, Models, Frontend-Pages, Tasks)
4. Erstelle einen konkreten Plan in `plan.md` im Projektroot
## Format von plan.md
```markdown
# Plan: [Titel der Anforderung]
## Kontext
Was ist das Problem / die Anforderung? Welche Teile des Systems sind betroffen?
## Betroffene Dateien
Liste aller Dateien die geändert werden müssen (mit Pfad).
## Tasks (in Reihenfolge)
### Task 1: [Titel]
- **Datei**: backend/app/...
- **Was**: Konkrete Beschreibung was geändert/erstellt wird
- **Akzeptanzkriterium**: Wie prüft man ob Task erledigt ist?
- **Abhängigkeiten**: keine / Task 2
### Task 2: ...
## Migrations-Check
Braucht es eine neue Alembic-Migration? (neue Spalten/Tabellen → ja)
## Reihenfolge-Empfehlung
Backend → Migration → Tests → Frontend
## Risiken / Offene Fragen
Was ist unklar? Was könnte schiefgehen?
```
## Projektspezifische Hinweise für den Plan
- **Celery Tasks**: Immer prüfen welche Queue (`step_processing` vs `thumbnail_rendering`)
- **Neue DB-Felder**: Migration nötig → in Plan als eigenen Task aufführen
- **Frontend API-Typen**: Jede neue Backend-Response braucht ein Interface in `frontend/src/api/*.ts`
- **Render-Pipeline-Änderungen**: step_processor.py → step_tasks.py → blender_render.py / still_render.py / turntable_render.py → schaeffler-still.js / schaeffler-turntable.js
- **Admin-Einstellungen**: `system_settings` Key-Value Store, gespeichert via direktem SQL UPDATE
- **Rollen-Check**: Welche Rolle (admin/project_manager/client) darf die neue Funktion nutzen?
Schreibe am Ende: "Plan fertig. Bitte mit `/implement` fortfahren."
+76
View File
@@ -0,0 +1,76 @@
# Review-Agent
Du bist der Reviewer für das Schaeffler Automat Projekt. Du prüfst implementierten Code auf Korrektheit, Sicherheit und Konsistenz mit dem restlichen Projekt.
## Dein Vorgehen
1. Lies `plan.md` — was sollte implementiert werden?
2. Lies alle geänderten Dateien
3. Prüfe gegen alle Checklisten unten
4. Schreibe einen Report in `review-report.md`
## Checklisten
### Backend / Python
- [ ] Neue Endpunkte haben Rollen-Check (`require_admin`, `require_admin_or_pm`, oder `get_current_user` + manueller Check)
- [ ] Keine SQL-Injections (ORM oder parameterisierte Queries)
- [ ] Pydantic-Input-Validierung für alle POST/PUT-Bodies
- [ ] Fehlerhafte IDs geben 404 (nicht 500)
- [ ] Neue Router in `main.py` registriert?
- [ ] Neue Models in `backend/app/models/__init__.py` importiert?
- [ ] Async-Konsistenz: FastAPI-Handler async, Celery-Tasks sync
### Celery / Tasks
- [ ] Task auf richtiger Queue? (`thumbnail_rendering` für Blender-Calls!)
- [ ] Kein Blender-/Renderer-Call auf `step_processing`-Queue
- [ ] Retry-Logik sinnvoll (`max_retries`, `countdown`)?
- [ ] Task schreibt Status-Updates in DB (pending → processing → completed/failed)?
### Datenbank
- [ ] Neue Felder haben Migration?
- [ ] Nullable-Felder korrekt deklariert (`nullable=True` + Optional in Schema)?
- [ ] Cascade-Deletes wo nötig (FK auf user/order → CASCADE)?
- [ ] `updated_at` wird bei Änderungen gesetzt?
### Frontend / TypeScript
- [ ] Neues API-Interface in `frontend/src/api/*.ts`?
- [ ] Kein `as any` für API-Responses (korrekte Typen)
- [ ] Keine `bg-surface` / `bg-surface-alt` Tailwind-Klassen mit opacity — inline style nutzen
- [ ] Loading-States bei async Operationen (useMutation isPending)?
- [ ] Fehler-Feedback für den Nutzer (Toast/Alert bei API-Fehlern)?
- [ ] Rollen-abhängige UI-Elemente korrekt versteckt?
### Render-Pipeline
- [ ] Neue Parameter durch alle Pipeline-Glieder gezogen?
(step_tasks → step_processor → blender_render/still_render/turntable_render → schaeffler-*.js)
- [ ] STL-Cache-Konvention eingehalten? (`{stem}_low.stl`, `{stem}_high.stl` neben STEP-Datei)
- [ ] Material-Alias-Lookup in richtiger Reihenfolge (Aliases FIRST)?
### Allgemein
- [ ] Kein hartcodierter Pfad (immer `UPLOAD_DIR` oder DB-Pfad nutzen)
- [ ] Keine Credentials im Code
- [ ] Englische Variablen/Kommentare im Code
- [ ] Keine `print()` in Produktion — `logging` nutzen
## Format review-report.md
```markdown
# Review Report: [Feature-Name]
Datum: [heute]
## Ergebnis: ✅ Freigabe / ⚠️ Kleinigkeiten / ❌ Blockierend
## Gefundene Probleme
### [Datei:Zeile] Beschreibung
**Schwere**: Kritisch / Mittel / Gering
**Empfehlung**: Was soll geändert werden?
## Positiv aufgefallen
...
## Empfehlung
Freigabe / Bitte [X] beheben und erneut reviewen.
```
Schreibe am Ende: "Review abgeschlossen. Ergebnis: [✅/⚠️/❌]"
+20
View File
@@ -0,0 +1,20 @@
import json, sys, subprocess
data = json.loads(sys.stdin.read())
# Nur nach Datei-Änderungen prüfen
if data.get("tool_name") in ["Write", "Edit"]:
results = []
# Tests
r = subprocess.run(["npm", "test", "--", "--passWithNoTests"], capture_output=True)
results.append(("Tests", r.returncode == 0))
# Linting
r = subprocess.run(["npm", "run", "lint"], capture_output=True)
results.append(("Lint", r.returncode == 0))
failed = [name for name, ok in results if not ok]
if failed:
print(f"⚠️ Quality Gate FAILED: {', '.join(failed)}", file=sys.stderr)
print("Bitte Fehler beheben bevor du fortfährst.", file=sys.stderr)
+12
View File
@@ -0,0 +1,12 @@
import json, sys
tool_input = json.loads(sys.stdin.read())
command = tool_input.get("tool_input", {}).get("command", "")
BLOCKED = ["rm -rf /", "dd if=", "mkfs", ":(){:|:&};:"]
for blocked in BLOCKED:
if blocked in command:
print(f"BLOCKED: Gefährlicher Befehl erkannt: {blocked}", file=sys.stderr)
sys.exit(2) # Exit-Code 2 = Operation blockiert
sys.exit(0)
View File
+19
View File
@@ -0,0 +1,19 @@
{
"permissions": {
"allow": ["Bash", "Read", "Write", "Edit"],
"deny": []
},
"hooks": {
"PreToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/pre_tool_use.py"
}
]
}
]
}
}
+28
View File
@@ -0,0 +1,28 @@
# Database
POSTGRES_DB=schaeffler
POSTGRES_USER=schaeffler
POSTGRES_PASSWORD=schaeffler
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
# Redis
REDIS_URL=redis://redis:6379/0
# JWT
JWT_SECRET_KEY=your-secret-key-here-change-in-production
JWT_ALGORITHM=HS256
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=480
# Azure OpenAI
AZURE_OPENAI_API_KEY=your-azure-openai-key
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_DEPLOYMENT=gpt-4o
AZURE_OPENAI_API_VERSION=2024-02-01
# File Storage
UPLOAD_DIR=/app/uploads
MAX_UPLOAD_SIZE_MB=500
# Celery worker concurrency (default: 8 parallel CAD jobs per worker container)
# Scale horizontally with: docker compose up --scale worker=N
CELERY_WORKER_CONCURRENCY=8
+7
View File
@@ -0,0 +1,7 @@
node_modules/
.env
.env.local
.DS_Store
*.log
core
/blender-renderer/core
+174
View File
@@ -0,0 +1,174 @@
# Schaeffler Automat
## Ziel
Automatisiertes Render-System für Schaeffler-Produktbilder. Kunden (intern) laden Excel-Auftragslisten hoch, das System extrahiert Produktdaten, verknüpft STEP-CAD-Dateien, rendert Thumbnails und Animationen über Blender (Cycles/EEVEE) oder Flamenco, und liefert fertige PNG/MP4-Ausgaben.
## Tech Stack
- **Backend**: Python 3.11, FastAPI (async), SQLAlchemy 2 (async), Alembic, Celery, Pydantic v2
- **Frontend**: React 18, TypeScript, Vite, Tailwind CSS, lucide-react
- **Datenbank**: PostgreSQL 16
- **Queue/Cache**: Redis 7 (Celery Broker + Backend)
- **Renderer**: Blender 5.0.1 (headless), cadquery (STEP→STL), Three.js (Playwright)
- **Render Farm**: Flamenco 3.8 (Manager + Worker, für Animationen)
- **Deployment**: Docker Compose (11 Services)
## Services (docker-compose.yml)
| Service | Port | Funktion |
|---|---|---|
| `postgres` | 5432 | Primärdatenbank |
| `redis` | 6379 | Celery Broker |
| `backend` | 8888 | FastAPI App (uvicorn) |
| `worker` | | Celery Worker, Queue: `step_processing`, concurrency=8 |
| `worker-thumbnail` | | Celery Worker, Queue: `thumbnail_rendering`, **concurrency=1** |
| `beat` | | Celery Beat (Scheduler) |
| `blender-renderer` | 8100 | Blender HTTP-Service (STEP→PNG, STEP→STL) |
| `threejs-renderer` | 8101 | Three.js/Playwright HTTP-Service |
| `flamenco-manager` | 8080 | Flamenco Job Manager |
| `flamenco-worker` | | Flamenco Render Worker (GPU) |
| `frontend` | 5173 | React/Vite Dev Server |
## Starten / Stoppen
```bash
# Alle Services starten
docker compose up -d
# Logs einzelner Services
docker compose logs -f backend
docker compose logs -f worker
docker compose logs -f worker-thumbnail
docker compose logs -f blender-renderer
# Neubauen nach Codeänderungen (Backend/Worker)
docker compose up -d --build backend worker worker-thumbnail
# Frontend-Änderungen: Hot-Reload aktiv, kein Rebuild nötig
```
## Standard-Zugangsdaten (Entwicklung)
- **Admin**: admin@schaeffler.com / Admin1234!
- **Backend API**: http://localhost:8888/docs
- **Frontend**: http://localhost:5173
- **Flamenco Manager**: http://localhost:8080
## Projektstruktur
```
schaefflerautomat/
├── backend/
│ ├── app/
│ │ ├── api/routers/ # FastAPI Router (admin, cad, orders, products, ...)
│ │ ├── models/ # SQLAlchemy ORM-Modelle (14 Modelle)
│ │ ├── schemas/ # Pydantic In/Out-Schemas
│ │ ├── services/ # Business-Logik (excel_parser, step_processor, ...)
│ │ ├── tasks/ # Celery Tasks (step_tasks.py, flamenco_tasks.py)
│ │ └── utils/ # Auth, Seeding
│ ├── alembic/versions/ # DB-Migrationen (001026+)
│ └── start.sh # Entrypoint: migrate → seed → uvicorn
├── frontend/src/
│ ├── api/ # API-Client-Funktionen (axios-basiert)
│ ├── components/ # Wiederverwendbare UI-Komponenten
│ └── pages/ # Seitenkomponenten
├── blender-renderer/ # Blender HTTP-Microservice (Python Flask)
├── threejs-renderer/ # Three.js/Playwright Microservice (Python Flask)
├── flamenco/ # Flamenco Dockerfile + Job-Type-Scripts (.js)
└── docker-compose.yml
```
## Coding-Standards
- **Sprache im Code**: Englisch (Variablen, Kommentare, Commits)
- **Commits**: Conventional Commits (`feat:`, `fix:`, `refactor:`, `docs:`)
- **Python**: async/await durchgehend im Backend, sync-Wrapper für Celery-Tasks
- **TypeScript**: Interfaces für alle API-Responses in `frontend/src/api/*.ts`
- **Keine Tests**: Aktuell kein automatisiertes Test-Suite vorhanden
## Datenbank-Migrationen
```bash
# Neue Migration erstellen
docker compose exec backend alembic revision --autogenerate -m "beschreibung"
# Migrationen anwenden
docker compose exec backend alembic upgrade head
# Status prüfen
docker compose exec backend alembic current
```
## Celery Task-Queues
| Queue | Worker | Concurrency | Tasks |
|---|---|---|---|
| `step_processing` | `worker` | 8 | `process_step_file`, `render_order_line_task`, `dispatch_order_line_render` |
| `thumbnail_rendering` | `worker-thumbnail` | 1 | `render_step_thumbnail`, `regenerate_thumbnail`, `generate_stl_cache` |
| `ai_validation` | `worker` | 8 | Azure AI Validierung |
**Wichtig**: `thumbnail_rendering` läuft mit concurrency=1, weil der blender-renderer nur 1 Request gleichzeitig verarbeiten kann. Mehr parallele Requests führen zu Timeouts.
## STEP-Processing-Pipeline
1. **Upload**: STEP-Datei hochladen → `CadFile`-Record erstellt → `process_step_file` Task eingereiht
2. **Metadata** (`process_step_file` auf `step_processing`):
- STEP-Objekte extrahieren (cadquery, ~0.1s)
- `parsed_objects` in DB speichern
- glTF konvertieren (falls konfiguriert)
- Status: `processing` → queut `render_step_thumbnail`
3. **Thumbnail** (`render_step_thumbnail` auf `thumbnail_rendering`):
- Blender oder Three.js renderer aufrufen
- STL-Cache erstellen: `{step_stem}_low.stl`, `{step_stem}_high.stl`
- Status: `completed` oder `failed`
- Materialien auto-populated
## STL-Cache-Konvention
STL-Dateien liegen **neben der STEP-Datei**:
```
uploads/{cad_file_id}/filename_low.stl
uploads/{cad_file_id}/filename_high.stl
```
Beim nächsten Render-Aufruf wird der Cache genutzt (keine Neu-Konvertierung).
## Material-Alias-System
- Materialien werden per STEP-Part-Name auf Schaeffler-Bibliotheksmaterialien (`SCHAEFFLER_...`) gemappt
- Lookup-Reihenfolge: **Alias-Tabelle zuerst**, dann exakter `Material.name`-Match, dann Pass-through
- Alias-Seeding: Admin → "Seed Aliases" oder via `POST /api/materials/seed-aliases`
- Neue Aliases direkt in DB oder über Material-Detail-UI hinzufügen
## Rollen
| Rolle | Berechtigungen |
|---|---|
| `admin` | Vollzugriff, Admin-Panel, alle Einstellungen |
| `project_manager` | Aufträge, Analytics, Render-Trigger, STL-Download |
| `client` | Eigene Aufträge anlegen und einsehen |
## Wichtige API-Endpoints
- `POST /api/uploads/excel` — Excel-Auftragsliste importieren
- `POST /api/orders/{id}/submit` — Auftrag einreichen
- `POST /api/orders/{id}/dispatch-renders` — Alle Render-Zeilen dispatchen
- `GET /api/cad/{id}/thumbnail` — Thumbnail (kein Auth, UUID opaque)
- `POST /api/cad/{id}/generate-stl/{quality}` — STL-Generierung manuell triggern
- `POST /api/admin/settings/regenerate-thumbnails` — Alle Thumbnails neu rendern
- `POST /api/admin/settings/process-unprocessed` — Unverarbeitete STEP-Dateien queuen
- `POST /api/admin/settings/generate-missing-stls` — Fehlende STL-Caches erstellen
- `GET /api/worker/activity` — Letzte 30 STEP-Verarbeitungen (Status, Timing)
## Bekannte Eigenheiten
- **Backend-Port 8888** (nicht 8000 — war belegt)
- **Tailwind CSS-Variablen**: `bg-surface` etc. funktionieren nicht mit `/ opacity`-Syntax wenn CSS-Variable einen Hex-Wert enthält. Stattdessen `style={{ backgroundColor: 'var(--color-bg-surface)' }}` verwenden.
- **Blender mm→m**: STEP-Dateien sind in mm, Blender intern in m. Alle Import-Scripts skalieren mit `0.001`.
- **Flamenco GPU**: `deploy.resources.reservations.devices` in docker-compose für NVIDIA-Support.
- **`settings_persistence`**: Admin-Einstellungen werden via direktem SQL-UPDATE gespeichert (nicht ORM-Mutation), da SQLAlchemy bei key-value-Stores keine Mutation trackt.
## Learnings-Pflicht
Nach jedem gelösten Problem oder jeder wichtigen Entscheidung:
→ Trag das Learning in LEARNINGS.md ein (Format: Datum | Kategorie | Problem → Lösung)
→ Commitiere LEARNINGS.md zusammen mit dem Fix: `docs: learning erfasst - [kurzbeschreibung]`
@@ -0,0 +1 @@
,hartmut,tuxedo-os,01.03.2026 21:23,file:///home/hartmut/.config/libreoffice/4;
@@ -0,0 +1,6 @@
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",,,,,
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Platte / Plate,Material Platte,Schraube / Screw,Material screw,Nut BZ,Material nut
Linearsysteme,Laufrollenführungen,Endplatten für Führungsschiene LFS,233092AM41,ANS.LFS52,,ANS.LFS52,ans_lfs52_p_thread.stp,ans_lfs52_p_online,linear,1,ANS_LFS52-0011.prt,Stahl brüniert,ISO4762-M6X12_010.prt,Stahl v2,,
Linearsysteme,Laufrollenführungen,Endplatten für Führungsschiene LFS,233092AM41,ANS.LFS52-FH,,ANS.LFS52-FH,ans_lfs52-fh_p_thread.stp,ans_lfs52-fh_p_online,linear,1,ANS_LFS52-FH-0011_P.prt,Stahl brüniert,,,,
Linearsysteme,Laufrollenführungen,Endplatten für Führungsschiene LFS,233092AM41,ANS.LFS86-C,,ANS.LFS86-C,ans_lfs86-c_p_thread.stp,ans_lfs86-c_p_online,linear,1,ANS_LFS86-C-0011.prt,Stahl brüniert,ISO4762-M6X25_002.prt,Stahl v2,,
1 Die Spalten ab A bis "Start" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und "Start" neue Spalten eingefügt werden. START diese Spalte bleibt leer Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. "Gewähltes Produkt" muss eindeutig sein. Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.
2 Ebene1 Ebene2 Baureihe PIM-ID (Klasse) Produkt (Baureihe) Gewähltes Produkt Name CAD-Modell Gewünschte Bildnummer Lagertyp Medias-Rendering Platte / Plate Material Platte Schraube / Screw Material screw Nut BZ Material nut
3 Linearsysteme Laufrollenführungen Endplatten für Führungsschiene LFS 233092AM41 ANS.LFS52 ANS.LFS52 ans_lfs52_p_thread.stp ans_lfs52_p_online linear 1 ANS_LFS52-0011.prt Stahl brüniert ISO4762-M6X12_010.prt Stahl v2
4 Linearsysteme Laufrollenführungen Endplatten für Führungsschiene LFS 233092AM41 ANS.LFS52-FH ANS.LFS52-FH ans_lfs52-fh_p_thread.stp ans_lfs52-fh_p_online linear 1 ANS_LFS52-FH-0011_P.prt Stahl brüniert
5 Linearsysteme Laufrollenführungen Endplatten für Führungsschiene LFS 233092AM41 ANS.LFS86-C ANS.LFS86-C ans_lfs86-c_p_thread.stp ans_lfs86-c_p_online linear 1 ANS_LFS86-C-0011.prt Stahl brüniert ISO4762-M6X25_002.prt Stahl v2
@@ -0,0 +1,8 @@
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Innenring Name: CAD,Innenring Material,Außenring Name: CAD,Außenring Material ,Rollen Name: CAD,Rollen Material,Käfig Name: CAD,Käfig Material,Käfig 2/ Deckel Name : CAD,Käfig Material,Dichtung Name: CAD,Dichtung Material,Dichtung 2 Name: CAD,Material Dichtung,Halteringe Name: CAD,Halteringe Material ,Scheibenpaket Name: CAD,Scheibenpaket Material,Sprengring Name: CAD,Sprengring Material ,Bordscheibe Name: CAD,Bordscheibe Material ,Sicherungsring Name: CAD,Sicherungsring Material ,Bolt,Material Bold,Spacer,Spacer: material,Anlaufschreibe,Anlaufscheibe Material
Wälz- und Gleitlager,Rollenlager,Axial-Zylinderrollenlager,2305110101,811..-L,,811..-L,81113-l_cut.stp,81113-l_online,axial,1,WS81113_GEN_1_AF1_1.prt,Stahl v2,GS81113_GEN_1_AF1_1.prt,Stahl v2,LRB7P5X7P5_001_1_1_1.prt,Stahl v2,K81113L-11_GEFR_1_AF0_1.prt,Aluminium,,,,,,,,,,,,,,,,,,,,,,
Wälz- und Gleitlager,Rollenlager,Axial-Zylinderrollenlager,2305110102,893..-M,,893..-M,89320-m-p5_cut.stp,89320-m-p5_online,axial,1,WS89320_GEN_1.prt,Stahl v2,GS89320_GEN_1.prt,Stahl v2,LRB13X13_001_1.prt,Stahl v2,K89320-31_MONTAGE_1.prt,Messing,K89320-M-11_MONTAGE_1.prt,Messing,,,,,,,,,,,,,,,ISO8750-2X10_003_1.prt,Stahl v2,,,,
Wälz- und Gleitlager,Rollenlager,Axial-Schrägrollenlager,230511AC32,AXS..,,AXS..,axs1220_cut.stp,axs1220_online,axial,1,F-235143-11_1_AF1_1.prt,Stahl v2,F-235143-11_1_AF0_1.prt,Stahl v2,NRB1P5X2P2_1_1.prt,Stahl v2,F-235143-31_1_AF0_1.prt,Generisch Plastik: schwarz,,,,,,,,,,,,,,,,,,,,,,
Wälz- und Gleitlager,Zubehör,Axiallagerscheiben,2305BF0201,GS811,,GS811,gs81152-01.stp,gs81152-01_online,axial,1,GS81152-01.prt,Stahl v2,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Wälz- und Gleitlager,Rollenlager,Zylinderrollenlager,2305090102,HCNU10..-XL-M1,,HCNU10..-XL-M1,nu1040-xl-m1_00_04_cut.stp,nu1040-xl-m1_00_04_online,radial,1,IR_NU1040-3001_21_04_1.prt,Stahl v2,AU_NU1040-3101_11_04_1.prt,Stahl v2,ZRB26X26_DUM_1.prt,Keramik,RKKM_N1040-A-M3-1031-P_1.prt,Messing,RKDK_N1040-A-M3-1041-P_1.prt,Messing,,,,,,,,,,,,,,,,,,,,
1 Die Spalten ab A bis "Start" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und "Start" neue Spalten eingefügt werden. START diese Spalte bleibt leer Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. "Gewähltes Produkt" muss eindeutig sein. Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.
2 Ebene1 Ebene2 Baureihe PIM-ID (Klasse) Produkt (Baureihe) Gewähltes Produkt Name CAD-Modell Gewünschte Bildnummer Lagertyp Medias-Rendering Innenring Name: CAD Innenring Material Außenring Name: CAD Außenring Material Rollen Name: CAD Rollen Material Käfig Name: CAD Käfig Material Käfig 2/ Deckel Name : CAD Käfig Material Dichtung Name: CAD Dichtung Material Dichtung 2 Name: CAD Material Dichtung Halteringe Name: CAD Halteringe Material Scheibenpaket Name: CAD Scheibenpaket Material Sprengring Name: CAD Sprengring Material Bordscheibe Name: CAD Bordscheibe Material Sicherungsring Name: CAD Sicherungsring Material Bolt Material Bold Spacer Spacer: material Anlaufschreibe Anlaufscheibe Material
3 Wälz- und Gleitlager Rollenlager Axial-Zylinderrollenlager 2305110101 811..-L 811..-L 81113-l_cut.stp 81113-l_online axial 1 WS81113_GEN_1_AF1_1.prt Stahl v2 GS81113_GEN_1_AF1_1.prt Stahl v2 LRB7P5X7P5_001_1_1_1.prt Stahl v2 K81113L-11_GEFR_1_AF0_1.prt Aluminium
4 Wälz- und Gleitlager Rollenlager Axial-Zylinderrollenlager 2305110102 893..-M 893..-M 89320-m-p5_cut.stp 89320-m-p5_online axial 1 WS89320_GEN_1.prt Stahl v2 GS89320_GEN_1.prt Stahl v2 LRB13X13_001_1.prt Stahl v2 K89320-31_MONTAGE_1.prt Messing K89320-M-11_MONTAGE_1.prt Messing ISO8750-2X10_003_1.prt Stahl v2
5 Wälz- und Gleitlager Rollenlager Axial-Schrägrollenlager 230511AC32 AXS.. AXS.. axs1220_cut.stp axs1220_online axial 1 F-235143-11_1_AF1_1.prt Stahl v2 F-235143-11_1_AF0_1.prt Stahl v2 NRB1P5X2P2_1_1.prt Stahl v2 F-235143-31_1_AF0_1.prt Generisch Plastik: schwarz
6 Wälz- und Gleitlager Zubehör Axiallagerscheiben 2305BF0201 GS811 GS811 gs81152-01.stp gs81152-01_online axial 1 GS81152-01.prt Stahl v2
7 Wälz- und Gleitlager Rollenlager Zylinderrollenlager 2305090102 HCNU10..-XL-M1 HCNU10..-XL-M1 nu1040-xl-m1_00_04_cut.stp nu1040-xl-m1_00_04_online radial 1 IR_NU1040-3001_21_04_1.prt Stahl v2 AU_NU1040-3101_11_04_1.prt Stahl v2 ZRB26X26_DUM_1.prt Keramik RKKM_N1040-A-M3-1031-P_1.prt Messing RKDK_N1040-A-M3-1041-P_1.prt Messing
Binary file not shown.
@@ -0,0 +1,7 @@
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",,,,,,,,,,,,,,,,,,,,,,,,,
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Innenring / Inner ring,Material Innenring,Außenring / Outer ring,Material Außenring,Außenring 2,Material Außenring 2,Gehause / Housing,Material housing,Sliding Layer,Material Sliding layer,Ringe,Material ringe,Dichtungsträger / Sealing carrier,Material Dichtungsträger,Dichtlippe / Sealing lip,Material Dichtlippe,Nipple,Material Nippel,Schraube / Screw,Material Schraube,Distanz bucshe / Spacer washer.prt,Material Distanz bucshe,Snapring,Material Snapring,Split Stift,Material split stift
Wälz- und Gleitlager,Gleitlager,Gleitbuchsen,,EGB..-E40-B,,EGB..-E40-B,egb3520-e40_asm.stp,egb3520-e40_online,radial,1,,,EGB3520-E40.prt,Bronze v2,,,,,,,,,,,,,,,,,,,,,,
Wälz- und Gleitlager,Gleitlager,Gelenklager,,GE..-HF,,GE..-HF,ge360-hf_000_p_cut.stp,ge360-hf_000_p_online,radial,1,GE360-HF-0021-EIN_1_AF0_1.prt,Durotect CMT,GE360-HF-0011-EIN_HAELFTE_AF0_1.prt,Stahl,GE360-HF-0011-EIN_HAELFTE_AF1_1.prt,Stahl v2,,,GE360-HF-0051-EIN_1_AF0_1.prt,GFK + PTFE,GE360-HF-0051-EIN_1_AF4_1.prt,Stahl v2,,,,,GE360-HF-0051-EIN_1_AF7_1.prt,Stahl v2,ISO8734-6X40_005_1_1.prt,Stahl v2,,,,,,
Wälz- und Gleitlager,Gleitlager,Gelenklager,,GE..-HO,,GE..-HO,ge120-ho_cut.stp,ge120-ho_online,radial,1,GE120-HO-0021-EIN_1_AF0_1.prt,Durotect M,GE120-DO-0011-EIN_1_AF0_1.prt,Durotect M,,,,,,,,,,,,,,,,,,,,,,
Wälz- und Gleitlager,Gleitlager,Gelenklager,,GE..-LO,,GE..-LO,ge20_lo_cut.stp,ge20_lo_online,radial,1,GE20-LO-0021-EIN_1_AF0_1.prt,Durotect M,GE20-DO-0011-EIN_1_AF0_1.prt,Durotect M,,,,,,,,,,,,,,,,,,,,,,
1 Die Spalten ab A bis "Start" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und "Start" neue Spalten eingefügt werden. START diese Spalte bleibt leer Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. "Gewähltes Produkt" muss eindeutig sein. Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.
2 Ebene1 Ebene2 Baureihe PIM-ID (Klasse) Produkt (Baureihe) Gewähltes Produkt Name CAD-Modell Gewünschte Bildnummer Lagertyp Medias-Rendering Innenring / Inner ring Material Innenring Außenring / Outer ring Material Außenring Außenring 2 Material Außenring 2 Gehause / Housing Material housing Sliding Layer Material Sliding layer Ringe Material ringe Dichtungsträger / Sealing carrier Material Dichtungsträger Dichtlippe / Sealing lip Material Dichtlippe Nipple Material Nippel Schraube / Screw Material Schraube Distanz bucshe / Spacer washer.prt Material Distanz bucshe Snapring Material Snapring Split Stift Material split stift
3 Wälz- und Gleitlager Gleitlager Gleitbuchsen EGB..-E40-B EGB..-E40-B egb3520-e40_asm.stp egb3520-e40_online radial 1 EGB3520-E40.prt Bronze v2
4 Wälz- und Gleitlager Gleitlager Gelenklager GE..-HF GE..-HF ge360-hf_000_p_cut.stp ge360-hf_000_p_online radial 1 GE360-HF-0021-EIN_1_AF0_1.prt Durotect CMT GE360-HF-0011-EIN_HAELFTE_AF0_1.prt Stahl GE360-HF-0011-EIN_HAELFTE_AF1_1.prt Stahl v2 GE360-HF-0051-EIN_1_AF0_1.prt GFK + PTFE GE360-HF-0051-EIN_1_AF4_1.prt Stahl v2 GE360-HF-0051-EIN_1_AF7_1.prt Stahl v2 ISO8734-6X40_005_1_1.prt Stahl v2
5 Wälz- und Gleitlager Gleitlager Gelenklager GE..-HO GE..-HO ge120-ho_cut.stp ge120-ho_online radial 1 GE120-HO-0021-EIN_1_AF0_1.prt Durotect M GE120-DO-0011-EIN_1_AF0_1.prt Durotect M
6 Wälz- und Gleitlager Gleitlager Gelenklager GE..-LO GE..-LO ge20_lo_cut.stp ge20_lo_online radial 1 GE20-LO-0021-EIN_1_AF0_1.prt Durotect M GE20-DO-0011-EIN_1_AF0_1.prt Durotect M
Binary file not shown.
@@ -0,0 +1,13 @@
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Innenring / Inner ring Name: CAD,Material Innenring,Innenring / Inner ring 2 Name: CAD,Material Innenring 2.,Außenring / Outer ring Name: CAD,Material Außenring,Wälzkörper / Rolling Element Name: CAD,Material: Wälzkörper / Rolling Element,Käfig / Cage Name: CAD,Material: Käfig / Cage,Käfig / Cage Name: CAD,Material: Käfig / Cage,Dichtungskern/Dichtungsträger Name: CAD,Material: Dichtungskern/Dichtungsträger,Dichtung Außen / Dichtlippe Name: CAD,Material: Dichtung Außen / Dichtlippe,Innen Buchse Name Name: CAD,Material: Innen Buchse,Sprengring Name: CAD,Material: Sprengring,Axial - WS Name: CAD,Material: Axial - WS,Axial - GS Name: CAD,Material: Axial GS,Schrauben Name: CAD,Material: Schrauben,Niet Name: CAD,Material: Niet,Unterlegscheibe,Material_Unterlegscheibe
Wälz- und Gleitlager,Kugellager,Axial-Rillenkugellager,2305100101,511,,51110-P6,51110-p6_a00_cut.stp,51110_online,axial,1,,,,,,,KUG7P144_1.prt,Stahl v2,AKUK_51110-A-JP-3001_H_1.prt,Stahlblech v2,,,,,,,,,,,WS_51110-3001_H_1.prt,Stahl v2,GS_51110-3001_H_1.prt,Stahl v2,,,,,,
Wälz- und Gleitlager,Kugellager,Axial-Rillenkugellager,2305100101,514..-MP,,51413-MP,51413-mp_p_cut.stp,51413-MP_online,axial,1,,,,,,,KUP26P988_1.prt,Stahl v2,AKUK51413-MP-0031_1.prt,Messing,,,,,,,,,,,WS51413-0021_1.prt,Stahl v2,GS51413-0011_1.prt,Stahl v2,,,,,,
Wälz- und Gleitlager,Kugellager,Axial-Rillenkugellager,2305100102,522..,,52211,52211_z00_cut.stp,52211_online,axial,1,,,,,,,KUG12P5_1.prt,Stahl v2,AKUK_51211-JP-3001_MONT_1.prt,Stahlblech v2,,,,,,,,,,,WS_52211-3001_H_1.prt,Stahl v2,GS_51211-3001_H_1.prt,Stahl v2,,,,,,
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-2RSR-N,,6205-2rsr-n-c3,6205-2rsr-n-c3_p_cut.stp,6205-2rsr-n-c3_online,radial,1,IR6205-1021_1_1.prt,Stahl v2,,,AU6205-2Z-0011_H1_1_1.prt,Stahl v2,KUG7P938_002_1_1.prt,Stahl v2,KUH6205-JN-0031_1_1.prt,Stahlblech v2,,,D_6205-RSR-1051-NBR-ARM__1_1.prt,Stahl v2,D_6205-RSR-1051-NBR_1_1.prt,Generisch Gummi: schwarz,,,,,,,,,,,NTS153301-3-1_41X3_20_H_1_1_2_3.prt,Stahl v2,,
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-2Z-N,,6205-2z-n-c3,6205-2z-n-c3_p_cut.stp,6205-2z-n-c3_online,radial,1,IR6205-2BRS-0021_H1_1.prt,Stahl v2,,,AU6205-2Z-0011_H1_1.prt,Stahl v2,KUG7P938_002_1.prt,Stahl v2,6205-BK_H2_1.prt,Stahlblech v2,,,DS6205-ZRG-A-0051_H_1.prt,Stahl v2,,,,,,,,,,,,,,,,
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-C-2HRS,,6205-C-2HRS,6205-c-2hrs_p_010_cut.stp,6205-C-2HRS_online,radial,1,IR_6205-B-2Z-8101_81_08_1_1.prt,Stahl v2,,,AU_6205-B-2Z-8101_81_08_1_1.prt,Stahl v2,KUG7_938-G5_A00_P_1.prt,Stahl v2,KUH6205-JN-8001_Z00_1 .prt,Stahlblech v2,KUH6205-JN-7031_1.prt,Stahlblech v2,D_6205-HRS-8001-NBR_ARM_B00_1.prt,Stahl v2,D_6205-HRS-8001-NBR_B00_1.prt,Generisch Gummi: schwarz,,,,,,,,,,,NTS153301-3-1_41X3_2_H-MONT_1.prt,Stahl v2,,
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-C-2HRS-N,,6205-c-2hrs-n-c3,6205-c-2hrs-n-c3_p_010_cut.stp,6205-c-2hrs-n-c3_online,radial,1,IR_6205-B-2Z-8101_81_08_1_1_2.prt,Stahl v2,,,AU_6205-B-2Z-N-8101_11_08_1_1_2.prt,Stahl v2,KUG7_938-G5_A00_P_1_1.prt,Stahl v2,KUH6205-JN-8001_Z00_1_1_2.prt,Stahlblech v2,KUH6205-JN-7031_1_1_2_3.prt,Stahlblech v2,D_6205-HRS-8001-NBR_ARM_B00_1_1.prt,Stahl v2,D_6205-HRS-8001-NBR_B00_1_1.prt,Generisch Gummi: schwarz,,,,,,,,,,,NTS153301-3-1_41X3_2_H-MONT_1_1.prt,Stahl v2,,
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-C-2HRS-NR,,6205-c-2hrs-nr-c3,6205-c-2hrs-nr-c3_00_10_cut.stp,6205-c-2hrs-nr-c3_online,radial,1,IR_6205-B-2Z-8101_81_010_1.prt,Stahl v2,,,AU_6205-B-2Z-N-8101_11_08_1.prt,Stahl v2,KUG7_938_KBE_1_1.prt,Stahl v2,KUH6205-JN-8001_Z00_1_1.prt,Stahlblech v2,KUH6205-JN-7031_1_1.prt,Stahlblech v2,D_6205-HRS-8001-NBR_ARM_A00_1 .prt,Stahl v2,D_6205-HRS-8001-NBR_A00_1.prt,Generisch Gummi: schwarz,,,SP52_KBE_1.prt,Stahl v2,,,,,,,,,,
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-C-2Z-N,,6204-c-2z-n,6204-c-2z-n_00_10_cut.stp,6204-c-2z-n_online,radial,1,IR_6204-B-2Z-8101_81_08_1.prt,Stahl v2,,,AU_6204-B-2Z-N-8101_11_10_1.prt,Stahl v2,KUG7_938_KBE_1_1_2.prt,Stahl v2,KUH6204-JN-8001_1.prt,Stahlblech v2,,,DS_6204-8001_X00_1.prt,Stahl v2,DS_6204-8001_X00_1.prt,Stahl v2,,,,,,,,,,,NTS153301-3-1_18X3_05_1.prt,Stahl v2,,
Wälz- und Gleitlager,Kugellager,Rillenkugellager,2305080101,6..-C-2Z-NR,,6202-c-2z-nr,6202-c-2z-nr-r18-25_00_10_cut.stp,6202-c-2z-nr_online,radial,1,IR_6202-B-2Z-8101_21_08_1.prt,Stahl v2,,,AU_6202-B-2Z-N-8101_11_10_1.prt,Stahl v2,KUG6_KBE_1.prt,Stahl v2,KUH6202-JN-8001_Z00_1.prt,Stahlblech v2,KUH6202-JN-8001_1.prt,Stahlblech v2,DS_6202-8001_Z00_1.prt,Stahl v2,DS_6202-8001_Z00_1.prt,Stahl v2,,,SP35_KBE_1.prt,Stahl v2,,,,,,,,,,
1 Die Spalten ab A bis "Start" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und "Start" neue Spalten eingefügt werden. START diese Spalte bleibt leer Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. "Gewähltes Produkt" muss eindeutig sein. Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.
2 Ebene1 Ebene2 Baureihe PIM-ID (Klasse) Produkt (Baureihe) Gewähltes Produkt Name CAD-Modell Gewünschte Bildnummer Lagertyp Medias-Rendering Innenring / Inner ring Name: CAD Material Innenring Innenring / Inner ring 2 Name: CAD Material Innenring 2. Außenring / Outer ring Name: CAD Material Außenring Wälzkörper / Rolling Element Name: CAD Material: Wälzkörper / Rolling Element Käfig / Cage Name: CAD Material: Käfig / Cage Käfig / Cage Name: CAD Material: Käfig / Cage Dichtungskern/Dichtungsträger Name: CAD Material: Dichtungskern/Dichtungsträger Dichtung Außen / Dichtlippe Name: CAD Material: Dichtung Außen / Dichtlippe Innen Buchse Name Name: CAD Material: Innen Buchse Sprengring Name: CAD Material: Sprengring Axial - WS Name: CAD Material: Axial - WS Axial - GS Name: CAD Material: Axial GS Schrauben Name: CAD Material: Schrauben Niet Name: CAD Material: Niet Unterlegscheibe Material_Unterlegscheibe
3 Wälz- und Gleitlager Kugellager Axial-Rillenkugellager 2305100101 511 51110-P6 51110-p6_a00_cut.stp 51110_online axial 1 KUG7P144_1.prt Stahl v2 AKUK_51110-A-JP-3001_H_1.prt Stahlblech v2 WS_51110-3001_H_1.prt Stahl v2 GS_51110-3001_H_1.prt Stahl v2
4 Wälz- und Gleitlager Kugellager Axial-Rillenkugellager 2305100101 514..-MP 51413-MP 51413-mp_p_cut.stp 51413-MP_online axial 1 KUP26P988_1.prt Stahl v2 AKUK51413-MP-0031_1.prt Messing WS51413-0021_1.prt Stahl v2 GS51413-0011_1.prt Stahl v2
5 Wälz- und Gleitlager Kugellager Axial-Rillenkugellager 2305100102 522.. 52211 52211_z00_cut.stp 52211_online axial 1 KUG12P5_1.prt Stahl v2 AKUK_51211-JP-3001_MONT_1.prt Stahlblech v2 WS_52211-3001_H_1.prt Stahl v2 GS_51211-3001_H_1.prt Stahl v2
6 Wälz- und Gleitlager Kugellager Rillenkugellager 2305080101 6..-2RSR-N 6205-2rsr-n-c3 6205-2rsr-n-c3_p_cut.stp 6205-2rsr-n-c3_online radial 1 IR6205-1021_1_1.prt Stahl v2 AU6205-2Z-0011_H1_1_1.prt Stahl v2 KUG7P938_002_1_1.prt Stahl v2 KUH6205-JN-0031_1_1.prt Stahlblech v2 D_6205-RSR-1051-NBR-ARM__1_1.prt Stahl v2 D_6205-RSR-1051-NBR_1_1.prt Generisch Gummi: schwarz NTS153301-3-1_41X3_20_H_1_1_2_3.prt Stahl v2
7 Wälz- und Gleitlager Kugellager Rillenkugellager 2305080101 6..-2Z-N 6205-2z-n-c3 6205-2z-n-c3_p_cut.stp 6205-2z-n-c3_online radial 1 IR6205-2BRS-0021_H1_1.prt Stahl v2 AU6205-2Z-0011_H1_1.prt Stahl v2 KUG7P938_002_1.prt Stahl v2 6205-BK_H2_1.prt Stahlblech v2 DS6205-ZRG-A-0051_H_1.prt Stahl v2
8 Wälz- und Gleitlager Kugellager Rillenkugellager 2305080101 6..-C-2HRS 6205-C-2HRS 6205-c-2hrs_p_010_cut.stp 6205-C-2HRS_online radial 1 IR_6205-B-2Z-8101_81_08_1_1.prt Stahl v2 AU_6205-B-2Z-8101_81_08_1_1.prt Stahl v2 KUG7_938-G5_A00_P_1.prt Stahl v2 KUH6205-JN-8001_Z00_1 .prt Stahlblech v2 KUH6205-JN-7031_1.prt Stahlblech v2 D_6205-HRS-8001-NBR_ARM_B00_1.prt Stahl v2 D_6205-HRS-8001-NBR_B00_1.prt Generisch Gummi: schwarz NTS153301-3-1_41X3_2_H-MONT_1.prt Stahl v2
9 Wälz- und Gleitlager Kugellager Rillenkugellager 2305080101 6..-C-2HRS-N 6205-c-2hrs-n-c3 6205-c-2hrs-n-c3_p_010_cut.stp 6205-c-2hrs-n-c3_online radial 1 IR_6205-B-2Z-8101_81_08_1_1_2.prt Stahl v2 AU_6205-B-2Z-N-8101_11_08_1_1_2.prt Stahl v2 KUG7_938-G5_A00_P_1_1.prt Stahl v2 KUH6205-JN-8001_Z00_1_1_2.prt Stahlblech v2 KUH6205-JN-7031_1_1_2_3.prt Stahlblech v2 D_6205-HRS-8001-NBR_ARM_B00_1_1.prt Stahl v2 D_6205-HRS-8001-NBR_B00_1_1.prt Generisch Gummi: schwarz NTS153301-3-1_41X3_2_H-MONT_1_1.prt Stahl v2
10 Wälz- und Gleitlager Kugellager Rillenkugellager 2305080101 6..-C-2HRS-NR 6205-c-2hrs-nr-c3 6205-c-2hrs-nr-c3_00_10_cut.stp 6205-c-2hrs-nr-c3_online radial 1 IR_6205-B-2Z-8101_81_010_1.prt Stahl v2 AU_6205-B-2Z-N-8101_11_08_1.prt Stahl v2 KUG7_938_KBE_1_1.prt Stahl v2 KUH6205-JN-8001_Z00_1_1.prt Stahlblech v2 KUH6205-JN-7031_1_1.prt Stahlblech v2 D_6205-HRS-8001-NBR_ARM_A00_1 .prt Stahl v2 D_6205-HRS-8001-NBR_A00_1.prt Generisch Gummi: schwarz SP52_KBE_1.prt Stahl v2
11 Wälz- und Gleitlager Kugellager Rillenkugellager 2305080101 6..-C-2Z-N 6204-c-2z-n 6204-c-2z-n_00_10_cut.stp 6204-c-2z-n_online radial 1 IR_6204-B-2Z-8101_81_08_1.prt Stahl v2 AU_6204-B-2Z-N-8101_11_10_1.prt Stahl v2 KUG7_938_KBE_1_1_2.prt Stahl v2 KUH6204-JN-8001_1.prt Stahlblech v2 DS_6204-8001_X00_1.prt Stahl v2 DS_6204-8001_X00_1.prt Stahl v2 NTS153301-3-1_18X3_05_1.prt Stahl v2
12 Wälz- und Gleitlager Kugellager Rillenkugellager 2305080101 6..-C-2Z-NR 6202-c-2z-nr 6202-c-2z-nr-r18-25_00_10_cut.stp 6202-c-2z-nr_online radial 1 IR_6202-B-2Z-8101_21_08_1.prt Stahl v2 AU_6202-B-2Z-N-8101_11_10_1.prt Stahl v2 KUG6_KBE_1.prt Stahl v2 KUH6202-JN-8001_Z00_1.prt Stahlblech v2 KUH6202-JN-8001_1.prt Stahlblech v2 DS_6202-8001_Z00_1.prt Stahl v2 DS_6202-8001_Z00_1.prt Stahl v2 SP35_KBE_1.prt Stahl v2
Binary file not shown.
@@ -0,0 +1,5 @@
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Rail,Material Rail
Linearsysteme,Profilschienenführungen,Sechsreihige Kugelumlaufeinheiten,233092AB06,TKSD..,,TKSD..,tksd25x1290-g2-45-45_p.stp,tksd25x1290-g2-45-45_online,linear,1,TKSD25X1290-G2-45-45.prt,Stahl v2
Linearsysteme,Profilschienenführungen,Rollenumlaufeinheiten,233092AB21,TSX..-D,,TSX..-D,tsx25-d-g1-hj-gen.stp,tsx25-d-g1-hj_online,linear,1,TSX25D-G1-HJ-GEN.prt,Stahl v2
1 Die Spalten ab A bis "Start" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und "Start" neue Spalten eingefügt werden. START diese Spalte bleibt leer Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. "Gewähltes Produkt" muss eindeutig sein. Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.
2 Ebene1 Ebene2 Baureihe PIM-ID (Klasse) Produkt (Baureihe) Gewähltes Produkt Name CAD-Modell Gewünschte Bildnummer Lagertyp Medias-Rendering Rail Material Rail
3 Linearsysteme Profilschienenführungen Sechsreihige Kugelumlaufeinheiten 233092AB06 TKSD.. TKSD.. tksd25x1290-g2-45-45_p.stp tksd25x1290-g2-45-45_online linear 1 TKSD25X1290-G2-45-45.prt Stahl v2
4 Linearsysteme Profilschienenführungen Rollenumlaufeinheiten 233092AB21 TSX..-D TSX..-D tsx25-d-g1-hj-gen.stp tsx25-d-g1-hj_online linear 1 TSX25D-G1-HJ-GEN.prt Stahl v2
@@ -0,0 +1,6 @@
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",,,,,,,,,,,,,,,,,,,,,,,,,
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Innenring / Inner ring,Material Innenring,Außenring / Outer ring,Material Außenring,Käfig / Cage,Material Käfig,Käfig 2 / Cage 2,Material Käfig 2,Niet,Material Niet,Wälzkörper / Rolling element,Material Wälzkörper,Bordscheibe IR / Loose Lip IR,Material Bordscheibe IR,Führungsring AU / Loose Lip AU,Führungsring AU / Loose Lip AU,Dichtungsträger / Sealing carrier,Material Dichtungsträger,Dichtlippe / Sealing lip,Material Dichtlippe,Schraube / Screw,Material Schraube,Distanz bucshe / Spacer washer,Material Distanz bucshe,Snapring,Material Snapring
Wälz- und Gleitlager,Rollenlager,TORB,2305091390 ,C31..-XL-K-M,,C31..-XL-K-M,c3152-xl-k-m_cut.stp,c3152-xl-k-m_online,radial,1,IR_C3152-K-3001,Stahl v2,AU_C3152-3001,Stahl v2,RK_C3152-M-3001,Messing,,,,,TORO_C3152-3001,Stahl v2,,,,,,,,,,,,,,
Wälz- und Gleitlager,Rollenlager,Radial SRB,2305091102,241..-BE-XL-K30-H40,,241..-BE-XL-K30-H40,24148-be-xl-k30-h40_006_cut.stp,24148-be-xl-k30-h40_006_online,radial,1,IR_24148-BE1-K30-0021,Stahl v2,AU_24148-BE1-WA-H40-3001_006,Stahl v2,RK_24148-JPB-0031,Stahlblech v2,,,,,TORO_24148-BE1,Stahl v2,BO_24148_BE1,Stahl v2,,,,,,,,,,,,
Wälz- und Gleitlager,Rollenlager,Radial SRB,2305091103,WS222..-E1-XL-2RSR,,WS222..-E1-XL-2RSR,ws22215-e1-2rsr_006_cut.stp,ws22215-e1-2rsr_006_online,radial,1,IR_WS22215-E1-D-CA-WA-3001_006,Stahl v2,AU_WS22215-E1-D-WA-3001_006,Stahl v2,RK_22215-E-JPA-0031_006,Stahlblech v2,,,,,TORO_22215-E1A_SCT,Stahl v2,,,,,D_WS22215-E1-RSR-3001_A,Stahl v2,D_WS22215-E1-RSR-3001-RUBBER,Generisch Gummi: schwarz,,,,,,
1 Die Spalten ab A bis "Start" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und "Start" neue Spalten eingefügt werden. START diese Spalte bleibt leer Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. "Gewähltes Produkt" muss eindeutig sein. Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.
2 Ebene1 Ebene2 Baureihe PIM-ID (Klasse) Produkt (Baureihe) Gewähltes Produkt Name CAD-Modell Gewünschte Bildnummer Lagertyp Medias-Rendering Innenring / Inner ring Material Innenring Außenring / Outer ring Material Außenring Käfig / Cage Material Käfig Käfig 2 / Cage 2 Material Käfig 2 Niet Material Niet Wälzkörper / Rolling element Material Wälzkörper Bordscheibe IR / Loose Lip IR Material Bordscheibe IR Führungsring AU / Loose Lip AU Führungsring AU / Loose Lip AU Dichtungsträger / Sealing carrier Material Dichtungsträger Dichtlippe / Sealing lip Material Dichtlippe Schraube / Screw Material Schraube Distanz bucshe / Spacer washer Material Distanz bucshe Snapring Material Snapring
3 Wälz- und Gleitlager Rollenlager TORB 2305091390  C31..-XL-K-M C31..-XL-K-M c3152-xl-k-m_cut.stp c3152-xl-k-m_online radial 1 IR_C3152-K-3001 Stahl v2 AU_C3152-3001 Stahl v2 RK_C3152-M-3001 Messing TORO_C3152-3001 Stahl v2
4 Wälz- und Gleitlager Rollenlager Radial SRB 2305091102 241..-BE-XL-K30-H40 241..-BE-XL-K30-H40 24148-be-xl-k30-h40_006_cut.stp 24148-be-xl-k30-h40_006_online radial 1 IR_24148-BE1-K30-0021 Stahl v2 AU_24148-BE1-WA-H40-3001_006 Stahl v2 RK_24148-JPB-0031 Stahlblech v2 TORO_24148-BE1 Stahl v2 BO_24148_BE1 Stahl v2
5 Wälz- und Gleitlager Rollenlager Radial SRB 2305091103 WS222..-E1-XL-2RSR WS222..-E1-XL-2RSR ws22215-e1-2rsr_006_cut.stp ws22215-e1-2rsr_006_online radial 1 IR_WS22215-E1-D-CA-WA-3001_006 Stahl v2 AU_WS22215-E1-D-WA-3001_006 Stahl v2 RK_22215-E-JPA-0031_006 Stahlblech v2 TORO_22215-E1A_SCT Stahl v2 D_WS22215-E1-RSR-3001_A Stahl v2 D_WS22215-E1-RSR-3001-RUBBER Generisch Gummi: schwarz
Binary file not shown.
@@ -0,0 +1,8 @@
"Die Spalten ab A bis ""Start"" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und ""Start"" neue Spalten eingefügt werden.",,,,,"START
diese Spalte bleibt leer","Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. ""Gewähltes Produkt"" muss eindeutig sein.",,,,,"Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Ebene1,Ebene2,Baureihe,PIM-ID (Klasse),Produkt (Baureihe),,Gewähltes Produkt,Name CAD-Modell,Gewünschte Bildnummer,Lagertyp,Medias-Rendering,Innenring / Inner ring Name: CAD,Material Innenring,Innenring / Inner ring 2 Name: CAD,Material Innenring 2,Innenring / Inner ring 3 Name: CAD,Material Innenring 3,Zwischenring 4,Material Zwischenring 4,Zwischenring 5,Material Zwischenring 5,Außenring / Outer ring Name: CAD,Material Außenring,Zwischenring 1,Material Zwischenring 1,Außenring / Outer ring 2 Name: CAD,Material Außenring 2,Zwischenring 2,Material Zwischenring 2,Außenring / Outer ring 3 Name: CAD,Material Außenring 3,Zwischenring 3,Material Zwischenring 3,Außenring / Outer ring 4 Name: CAD,Material Außenring 4,Käfig / Cage Name: CAD,Material: Käfig / Cage,Käfig / Cage 2 Name: CAD,Material: Käfig / Cage 2,Käfig / Cage 3 Name: CAD,Material: Käfig / Cage 3,Käfig / Cage 4 Name: CAD,Material: Käfig / Cage 4,Käfig / Cage 5 Name: CAD,Material: Käfig / Cage 5,Käfig / Cage 6 Name: CAD,Material: Käfig / Cage 6,Käfig / Cage 7 Name: CAD,Material: Käfig / Cage 7,Käfig / Cage 8 Name: CAD,Material: Käfig / Cage 8,Käfigbolzen Name: CAD,Material: Käfigbolzen,Käfigbolzen 2 Name: CAD,Material: Käfigbolzen ,Bolzensstift Name: CAD,Material Bolzenstift,Wälzkörper / Rolling Element Name: CAD,Material: Wälzkörper / Rolling Element,Wälzkörper / Rolling Element 2 Name: CAD,Material: Wälzkörper / Rolling Element 2,Dichtungskern/Dichtungsträger Name: CAD,Material: Dichtungskern/Dichtungsträger,Dichtung Außen / Dichtlippe Name: CAD,Material: Dichtung Außen / Dichtlippe,Sealing ring Name: CAD,Material Sealing ring,Sealing ring 2 Name: CAD,Material: Sealing ring 2,Feder / Spring Name: CAD,Material: Feder / Spring,Innen Buchse Name Name: CAD,Material: Innen Buchse,Sprengring Name: CAD,Material: Sprengring,Axial - WS Name: CAD,Material: Axial - WS,Axial - GS Name: CAD,Material: Axial GS,Schrauben Name: CAD,Material: Schrauben,Niet Name: CAD,Material: Niet,Unterlegscheibe,Material: Unterlegscheibe,Stoßdämpfer Stopfen: CAD,Material: Stoßdämpfer Stopfen,Stoßdämpfer Feder: CAD,Material: Stoßdämpfer Feder,Stoßdämpfer Unterlegscheibe: CAD,Material: Stoßdämpfer Unterlegscheibe
Wälz- und Gleitlager,Rollenlager,Kegelrollenlager,2305091051,320..-X-XL-DF,,320..-X-XL-DF,32016-x-e1-df_00_cut.stp,32016-x-e1-df_00_online,radial,1,IR_32016-X-E1-WA-3001_21_0-1360.PRT,Stahl v2,IR_32016-X-E1-WA-3001_21_-19867.PRT,Stahl v2,,,,,,,AU_32016-X-E1-WA-3001_11_0-2017.PRT,Stahl v2,ZWR_32016-X-1071__1_AF0_1.PRT,Stahl v2,AU_32016-X-E1-WA-3001_11_-20520.PRT,Stahl v2,,,,,,,,,KRK_32016-X-JPB-1031_31_1_AF1_1.PRT,Stahl v2,KRK_32016-X-JPB-1031_31_1_AF0_1.PRT,Stahl v2,,,,,,,,,,,,,,,,,,,KERO_32017-X-E1-QPA-WA_00_1_1.PRT,Stahl v2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Wälz- und Gleitlager,Rollenlager,Kegelrollenlager,2305091021,F-802070.TR4-AM,,F-802070.TR4-AM,f-802070_tr4-am_04_cut.stp,f-802070_tr4-am_04_online,radial,1,F-802070-3001_IR_TR2_04_1_AF0_1.PRT,Stahl v2,F-802070-3001_IR_TR2_04_1_AF1_1.PRT,Stahl v2,,,F-802070-3001_IZWR_TR_04_1_AF0_.PRT,Stahl v2,,,F-802070-3101_AU_TR1_04_1_AF0_1.PRT,Stahl v2,F-802070-3001_AZWR_TR-S_04-6077.PRT,Stahl v2,F-802070-3101_AU_TR1_04_1_AF1_1.PRT,Stahl v2,F-802070-3001_AZWR_TR-AS_04_1_A.PRT,Stahl v2,F-802070-3101_AU_TR1_04_1_AF2_1.PRT,Stahl v2,F-802070-3001_AZWR_TR-S_0-12124.PRT,Stahl v2,F-802070-3101_AU_TR1_04_1_AF3_1.PRT,Stahl v2,F-802070-3101_KRSS_TR-F_1_AF0_1.PRT,Stahl v2,F-802070-3201_KRSS_TR-F_1_AF0_1.PRT,Stahl v2,F-802070-3201_KRSS_TR-F_1_AF3_1.PRT,Stahl v2,F-802070-3101_KRSS_TR-F_1_AF1_1.PRT,Stahl v2,F-802070-3101_KRSS_TR-F_1_AF2_1.PRT,Stahl v2,F-802070-3201_KRSS_TR-F_1_AF1_1.PRT,Stahl v2,F-802070-3201_KRSS_TR-F_1_AF2_1.PRT,Stahl v2,F-802070-3101_KRSS_TR-F_1_AF3_1.PRT,Stahl v2,F-802070-3101_BLZ_TR_1_1.PRT,Stahl v2,,,,,KERO_F-802070-M-QP_ISB_1_AF30_1.PRT,Stahl v2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Wälz- und Gleitlager,Rollenlager,Kegelrollenlager,2305091011,F-803422.01.TR2,,F-803422.01.TR2,f-803422_01_tr2_04_cut.stp,f-803422_01_tr2_04_online,radial,1,F-803422-1021_IR_TR2_04_1_AF0_1.PRT,Stahl v2,,,,,,,,,F-803422-3001_AU_TR1_WA_04-6311.PRT,Stahl v2,,,F-803422-3001_AU_TR1_WA_04-7785.PRT,Stahl v2,,,,,,,,,Z-536748_01-3001_KRK_T_A1-19091.PRT,Stahl v2,Z-536748_01-3001_KRK_T_A1-30636.PRT,Stahl v2,,,,,,,,,,,,,,,,,,,KERO_Z-513683-QP_ISB_1_1.PRT,Stahl v2,,,,,,,,,,,,,,,,,,,,,,,,,,,F-809724-0041_BLZ_TR_1_1.PRT,Stahl v2,F-809724-0081_TR_DUMMY_1_1.PRT,Stahl v2,HK1214-2RS-FPM-11_ALT_1_1.PRT,Stahl v2
Wälz- und Gleitlager,Rollenlager,Axial-Kegelrollenlager,2305110401,KT-SERIES(1),,KT-SERIES(1),kt1120_04_cut.stp,kt1120_04_online,axial,1,,,,,,,,,,,,,,,,,,,,,,,,,AKRSS_KT1120-F-3101_04_1_AF0_1.PRT,Stahl v2,AKRSS_KT1120-F-3201_04_1_AF0_1.PRT,Stahl v2,,,,,,,,,,,,,BLZ_KT1120-3001_04_1_1.PRT,Stahl v2,,,,,KERO_KT1120-M-QP-WEA_04_1_1.PRT,Stahl v2,,,,,,,,,,,,,,,,,WS_KT1120-3001_04_1_AF0_1.PRT,Stahl v2,WS_KT1120-3001_04_1_AF1_1.PRT,Stahl v2,,,,,,,,,,,,
Wälz- und Gleitlager,Rollenlager,Kegelrollenlager,2305BA0303,L320..-X-XL,,L320..-X-XL,l32016-x-xl_0d_00_cut.stp,l32016-x-xl_0d_00_online,radial,1,,,,,,,,,,,AU_L32016-X-XL-_0D_1_AF0_1.PRT,Stahl v2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
1 Die Spalten ab A bis "Start" stehen zur freien Verfügung, zum Beispiel für Bemerkungen oder zusätzliche Informationen. Die Überschriften sind frei wählbar. Es können zwischen A und "Start" neue Spalten eingefügt werden. START diese Spalte bleibt leer Bitte diese Überschriften nicht ändern und keine weiteren Spalten hinzufügen. Bitte alle Spalten ausfüllen. "Gewähltes Produkt" muss eindeutig sein. Bitte immer paarweise neue Spalten einfügen, Spalte 1 für den Produktteil (.prt) und Spalte 2 für das zugehörige Material. Die Überschriften der Spalten sind frei wählbar. Die Tabelle kann ab hier paarweise Endlos erweitert werden.
2 Ebene1 Ebene2 Baureihe PIM-ID (Klasse) Produkt (Baureihe) Gewähltes Produkt Name CAD-Modell Gewünschte Bildnummer Lagertyp Medias-Rendering Innenring / Inner ring Name: CAD Material Innenring Innenring / Inner ring 2 Name: CAD Material Innenring 2 Innenring / Inner ring 3 Name: CAD Material Innenring 3 Zwischenring 4 Material Zwischenring 4 Zwischenring 5 Material Zwischenring 5 Außenring / Outer ring Name: CAD Material Außenring Zwischenring 1 Material Zwischenring 1 Außenring / Outer ring 2 Name: CAD Material Außenring 2 Zwischenring 2 Material Zwischenring 2 Außenring / Outer ring 3 Name: CAD Material Außenring 3 Zwischenring 3 Material Zwischenring 3 Außenring / Outer ring 4 Name: CAD Material Außenring 4 Käfig / Cage Name: CAD Material: Käfig / Cage Käfig / Cage 2 Name: CAD Material: Käfig / Cage 2 Käfig / Cage 3 Name: CAD Material: Käfig / Cage 3 Käfig / Cage 4 Name: CAD Material: Käfig / Cage 4 Käfig / Cage 5 Name: CAD Material: Käfig / Cage 5 Käfig / Cage 6 Name: CAD Material: Käfig / Cage 6 Käfig / Cage 7 Name: CAD Material: Käfig / Cage 7 Käfig / Cage 8 Name: CAD Material: Käfig / Cage 8 Käfigbolzen Name: CAD Material: Käfigbolzen Käfigbolzen 2 Name: CAD Material: Käfigbolzen Bolzensstift Name: CAD Material Bolzenstift Wälzkörper / Rolling Element Name: CAD Material: Wälzkörper / Rolling Element Wälzkörper / Rolling Element 2 Name: CAD Material: Wälzkörper / Rolling Element 2 Dichtungskern/Dichtungsträger Name: CAD Material: Dichtungskern/Dichtungsträger Dichtung Außen / Dichtlippe Name: CAD Material: Dichtung Außen / Dichtlippe Sealing ring Name: CAD Material Sealing ring Sealing ring 2 Name: CAD Material: Sealing ring 2 Feder / Spring Name: CAD Material: Feder / Spring Innen Buchse Name Name: CAD Material: Innen Buchse Sprengring Name: CAD Material: Sprengring Axial - WS Name: CAD Material: Axial - WS Axial - GS Name: CAD Material: Axial GS Schrauben Name: CAD Material: Schrauben Niet Name: CAD Material: Niet Unterlegscheibe Material: Unterlegscheibe Stoßdämpfer Stopfen: CAD Material: Stoßdämpfer Stopfen Stoßdämpfer Feder: CAD Material: Stoßdämpfer Feder Stoßdämpfer Unterlegscheibe: CAD Material: Stoßdämpfer Unterlegscheibe
3 Wälz- und Gleitlager Rollenlager Kegelrollenlager 2305091051 320..-X-XL-DF 320..-X-XL-DF 32016-x-e1-df_00_cut.stp 32016-x-e1-df_00_online radial 1 IR_32016-X-E1-WA-3001_21_0-1360.PRT Stahl v2 IR_32016-X-E1-WA-3001_21_-19867.PRT Stahl v2 AU_32016-X-E1-WA-3001_11_0-2017.PRT Stahl v2 ZWR_32016-X-1071__1_AF0_1.PRT Stahl v2 AU_32016-X-E1-WA-3001_11_-20520.PRT Stahl v2 KRK_32016-X-JPB-1031_31_1_AF1_1.PRT Stahl v2 KRK_32016-X-JPB-1031_31_1_AF0_1.PRT Stahl v2 KERO_32017-X-E1-QPA-WA_00_1_1.PRT Stahl v2
4 Wälz- und Gleitlager Rollenlager Kegelrollenlager 2305091021 F-802070.TR4-AM F-802070.TR4-AM f-802070_tr4-am_04_cut.stp f-802070_tr4-am_04_online radial 1 F-802070-3001_IR_TR2_04_1_AF0_1.PRT Stahl v2 F-802070-3001_IR_TR2_04_1_AF1_1.PRT Stahl v2 F-802070-3001_IZWR_TR_04_1_AF0_.PRT Stahl v2 F-802070-3101_AU_TR1_04_1_AF0_1.PRT Stahl v2 F-802070-3001_AZWR_TR-S_04-6077.PRT Stahl v2 F-802070-3101_AU_TR1_04_1_AF1_1.PRT Stahl v2 F-802070-3001_AZWR_TR-AS_04_1_A.PRT Stahl v2 F-802070-3101_AU_TR1_04_1_AF2_1.PRT Stahl v2 F-802070-3001_AZWR_TR-S_0-12124.PRT Stahl v2 F-802070-3101_AU_TR1_04_1_AF3_1.PRT Stahl v2 F-802070-3101_KRSS_TR-F_1_AF0_1.PRT Stahl v2 F-802070-3201_KRSS_TR-F_1_AF0_1.PRT Stahl v2 F-802070-3201_KRSS_TR-F_1_AF3_1.PRT Stahl v2 F-802070-3101_KRSS_TR-F_1_AF1_1.PRT Stahl v2 F-802070-3101_KRSS_TR-F_1_AF2_1.PRT Stahl v2 F-802070-3201_KRSS_TR-F_1_AF1_1.PRT Stahl v2 F-802070-3201_KRSS_TR-F_1_AF2_1.PRT Stahl v2 F-802070-3101_KRSS_TR-F_1_AF3_1.PRT Stahl v2 F-802070-3101_BLZ_TR_1_1.PRT Stahl v2 KERO_F-802070-M-QP_ISB_1_AF30_1.PRT Stahl v2
5 Wälz- und Gleitlager Rollenlager Kegelrollenlager 2305091011 F-803422.01.TR2 F-803422.01.TR2 f-803422_01_tr2_04_cut.stp f-803422_01_tr2_04_online radial 1 F-803422-1021_IR_TR2_04_1_AF0_1.PRT Stahl v2 F-803422-3001_AU_TR1_WA_04-6311.PRT Stahl v2 F-803422-3001_AU_TR1_WA_04-7785.PRT Stahl v2 Z-536748_01-3001_KRK_T_A1-19091.PRT Stahl v2 Z-536748_01-3001_KRK_T_A1-30636.PRT Stahl v2 KERO_Z-513683-QP_ISB_1_1.PRT Stahl v2 F-809724-0041_BLZ_TR_1_1.PRT Stahl v2 F-809724-0081_TR_DUMMY_1_1.PRT Stahl v2 HK1214-2RS-FPM-11_ALT_1_1.PRT Stahl v2
6 Wälz- und Gleitlager Rollenlager Axial-Kegelrollenlager 2305110401 KT-SERIES(1) KT-SERIES(1) kt1120_04_cut.stp kt1120_04_online axial 1 AKRSS_KT1120-F-3101_04_1_AF0_1.PRT Stahl v2 AKRSS_KT1120-F-3201_04_1_AF0_1.PRT Stahl v2 BLZ_KT1120-3001_04_1_1.PRT Stahl v2 KERO_KT1120-M-QP-WEA_04_1_1.PRT Stahl v2 WS_KT1120-3001_04_1_AF0_1.PRT Stahl v2 WS_KT1120-3001_04_1_AF1_1.PRT Stahl v2
7 Wälz- und Gleitlager Rollenlager Kegelrollenlager 2305BA0303 L320..-X-XL L320..-X-XL l32016-x-xl_0d_00_cut.stp l32016-x-xl_0d_00_online radial 1 AU_L32016-X-XL-_0D_1_AF0_1.PRT Stahl v2
Binary file not shown.
Binary file not shown.
+116
View File
@@ -0,0 +1,116 @@
# Projekt-Learnings — Schaeffler Automat
## Format
**Datum | Kategorie | Problem → Lösung**
---
## Learnings
### 2026-01-15 | Architektur | Backend-Port-Konflikt
**Problem:** FastAPI standardmäßig auf Port 8000 — war auf dem Entwicklungsrechner belegt
**Lösung:** Port 8888 in `docker-compose.yml` und Vite-Proxy konfiguriert
**Für künftige Projekte:** Port früh festlegen und in CLAUDE.md dokumentieren
---
### 2026-01-20 | Datenbank | SQLAlchemy trackt key-value-Store-Mutations nicht
**Problem:** Admin-Einstellungen (`system_settings`) wurden via ORM gespeichert, Änderungen wurden nicht persistiert
**Ursache:** SQLAlchemy erkennt keine Mutation an einem bereits geladenen Objekt wenn nur ein Value-Feld geändert wird
**Lösung:** Direktes SQL `UPDATE` via `op.execute()` statt ORM-Mutation in `admin.py`
**Für künftige Projekte:** Key-Value-Stores immer mit direktem SQL oder `session.execute(update(...))` verwalten
---
### 2026-01-25 | Render-Pipeline | Blender ignoriert STEP-Einheiten (mm vs. m)
**Problem:** STEP-Dateien sind in Millimetern, Blender arbeitet intern in Metern → 50mm-Lager erscheint 50 Meter breit, Kamera framt falsch
**Lösung:** `_scale_mm_to_m(parts)` Helper in allen 3 Render-Scripts: `part.scale = (0.001, 0.001, 0.001)`, Transform anwenden
**Betroffene Dateien:** `blender_render.py`, `still_render.py`, `turntable_render.py`
**Für künftige Projekte:** Einheiten-Konvertierung direkt nach STL-Import, vor jeder Kamera-Kalkulation
---
### 2026-01-28 | Render-Pipeline | Blender 5.0 hat `scene.node_tree` entfernt
**Problem:** `_setup_bg_compositor()` rief `scene.node_tree` auf (in Blender 5.0 entfernt) → Python-Exception → Blender exitete mit Code 0 → Flamenco markierte Task fälschlicherweise als "completed"
**Lösung:** `_setup_bg_compositor()` aus Setup + Render-Script entfernt; bg_color-Kompositing in FFmpeg verschoben (`-f lavfi -i color=...` + overlay-Filter)
**Wichtig:** Immer `try: main() except SystemExit: raise except Exception: traceback; sys.exit(1)` in Blender-Scripts — sonst verschluckt Blender Python-Exceptions
**Für künftige Projekte:** Nach Blender-Major-Updates alle API-Calls prüfen; Exception-Guard ist Pflicht
---
### 2026-02-05 | Material-System | Material-Alias-Lookup-Reihenfolge falsch
**Problem:** `Steel--Stahl` war sowohl ein kanonischer `Material.name` als auch ein Alias für `SCHAEFFLER_010101_Steel-Bare`. Der Lookup prüfte zuerst den exakten Namen und fand `Steel--Stahl` — Blender konnte diesen Namen aber nicht in der Library finden
**Lösung:** Lookup-Reihenfolge in `material_service.py` umgekehrt: **Aliases zuerst**, dann exakter Name, dann Pass-through
**Für künftige Projekte:** Alias-System immer so designen dass Aliases Vorrang haben; nie zwei Lookup-Pfade mit überlappenden Treffern
---
### 2026-02-10 | Render-Pipeline | Blender-Template zerstört HDRI/World
**Problem:** Im Template-Modus (Mode B) wurden trotzdem Auto-Lights und eine neue World erstellt → überschrieb den HDRI aus dem .blend-Template → falsche Beleuchtung
**Ursache:** Auto-Licht- und World-Setup-Code lief bedingungslos, nicht nur im Mode A
**Lösung:** In Template-Mode werden Lights, World und Color-Management-Override vollständig übersprungen; nur die Kamera wird ggf. neu berechnet
**Betroffene Dateien:** `still_render.py`, `turntable_render.py`, `schaeffler-still.js`, `schaeffler-turntable.js`
---
### 2026-02-15 | Celery | Blender-Queue-Flooding durch falsche Concurrency
**Problem:** Alle Celery-Tasks (schnelle Metadata-Extraktion + langsamer Blender-Render) liefen auf `step_processing` mit concurrency=8 → 8 Workers schickten gleichzeitig Requests an blender-renderer (der nur 1 gleichzeitig verarbeiten kann) → 7 davon liefen in 300s-Timeout → blockierte die gesamte Queue
**Lösung:** Pipeline aufgeteilt:
- `process_step_file` (step_processing, concurrency=8): nur schnelle Metadata-Extraktion (<2s), queut dann →
- `render_step_thumbnail` (thumbnail_rendering, concurrency=1): Blender-Call, niemals timeout
**Neuer Service:** `worker-thumbnail` in `docker-compose.yml` mit `--concurrency=1`
**Für künftige Projekte:** HTTP-Services die nur 1 Request gleichzeitig verarbeiten können IMMER auf einer separaten Queue mit concurrency=1 laufen lassen
---
### 2026-02-18 | Frontend | Tailwind CSS-Variablen inkompatibel mit opacity-Syntax
**Problem:** `bg-surface/50` oder `bg-surface` (wenn `--color-bg-surface` ein Hex-Wert ist) generiert `rgb(var(--color-bg-surface) / 0.5)` — invalides CSS, weil `rgb()` keine Hex-Werte als Channel-Input akzeptiert → Hintergrund transparent
**Ursache:** Tailwind erwartet CSS-Variablen mit RGB-Channel-Format (`255 255 255`), nicht Hex (`#ffffff`)
**Lösung:** Inline-Style verwenden: `style={{ backgroundColor: 'var(--color-bg-surface)' }}`
**Für künftige Projekte:** Entweder CSS-Variablen im RGB-Channel-Format definieren, oder konsequent inline styles für variable Farben
---
### 2026-02-20 | STL-Cache | Three.js-Renderer nutzte tempfile → kein Download möglich
**Problem:** Three.js-Renderer konvertierte STEP→STL in ein tempfile und löschte es anschließend → STL-Download-Endpoint fand keine Datei
**Ursache:** Three.js war ursprünglich nur für Thumbnails gebaut, STL-Cache-Konvention (`{stem}_low.stl` neben STEP-Datei) wurde nicht implementiert
**Lösung:** Persistent cache path: `step_path.parent / f"{step_path.stem}_low.stl"`, cache-hit-check vor Konvertierung, kein `unlink()` mehr
**Für künftige Projekte:** STL-Cache-Konvention (`{step_stem}_{quality}.stl` neben STEP-Datei) von Anfang an in allen Renderer-Services einhalten
---
### 2026-02-20 | STL-Cache | blender-renderer fehlte /convert-stl Endpoint
**Problem:** Für Produkte die mit Blender gerendert wurden war kein STL-Cache vorhanden wenn nicht explizit gerendert wurde (blender-renderer renderte + konvertierte in einem Schritt, aber STL wurde nicht persistiert)
**Lösung:** Neuer `/convert-stl` Endpoint in `blender-renderer/app.py`: konvertiert STEP→STL ohne Render, persistiert Cache. Neuer Celery-Task `generate_stl_cache` auf `thumbnail_rendering`-Queue. Admin-Funktion "Generate Missing STLs" zum Batch-Nachfüllen
---
### 2026-02-22 | Material-System | Fehlender Alias blockiert Material-Replacement
**Problem:** Produkt F-803422.01.TR2 (SA-2026-00080) renderte ohne Materialersetzung. Material "Stahl v2" war korrekt in der UI gespeichert, aber weder in `materials` noch in `material_aliases` vorhanden
**Ursache:** Alias-Seeding aus Excel deckte nicht alle Varianten der deutschen Materialbezeichnungen ab
**Lösung:** Alias direkt in DB eingetragen: `"Stahl v2"``SCHAEFFLER_010101_Steel-Bare`
**Für künftige Projekte:** Bei Render ohne Materialersetzung immer zuerst `resolve_material_map()` debuggen und Alias-Tabelle prüfen; Alias-Seeding regelmäßig mit neuen Excel-Varianten erweitern
---
### 2026-02-25 | Frontend | canDispatch-Bedingung zu restriktiv
**Problem:** "Dispatch Renders"-Button war nicht sichtbar obwohl der Auftrag offene Render-Zeilen hatte
**Ursache:** `canDispatch` enthielt `&& hasRetryable` — Button erschien nur wenn pending/failed/cancelled-Zeilen vorhanden waren, nicht wenn alle Zeilen "pending" im Erstauftrag
**Lösung:** `hasRetryable`-Bedingung entfernt; Button ist immer sichtbar wenn Auftrag im richtigen Status und User privilegiert ist
**Für künftige Projekte:** Aktions-Buttons nicht zu stark von abgeleiteten Zuständen abhängig machen; lieber im Backend validieren
---
### 2026-02-28 | Frontend | MaterialInput-Dropdown ohne Hintergrund
**Problem:** Dropdown der Material-Suchfeld-Komponente erschien transparent — Text über dem Hintergrund kaum lesbar
**Ursache:** `bg-surface` Tailwind-Klasse + CSS-Variable mit Hex-Wert (siehe Learning 2026-02-18)
**Lösung:** `style={{ backgroundColor: 'var(--color-bg-surface)' }}` für Dropdown-Container, Group-Header und Sticky-Button
**Datei:** `frontend/src/components/shared/MaterialInput.tsx`
---
## Offene Fragen
- [ ] Azure AI Credentials für Phase 4 (Bildvalidierung) noch nicht konfiguriert
- [ ] Flamenco GPU-Support nur mit NVIDIA — AMD/CPU-Fallback fehlt
- [ ] Material-Alias-Seeding deckt noch nicht alle deutschen Materialbezeichnungs-Varianten ab
- [ ] Turntable-Animation: bg_color via FFmpeg-Overlay — Qualität bei Transparenz-Edges prüfen
+104
View File
@@ -0,0 +1,104 @@
"""Generate material_library.blend with all 35 Schaeffler standard materials.
Run with: blender --background --python generate_blend.py
"""
import bpy
import os
# Placeholder colors per material — tuned to approximate real-world appearance
# Format: (R, G, B, A) linear color, metallic, roughness
MATERIALS = [
# --- 01 Metals ---
("SCHAEFFLER_010101_Steel-Bare", (0.55, 0.56, 0.58, 1.0), 1.0, 0.35),
("SCHAEFFLER_010102_Steel-Burnished", (0.15, 0.12, 0.10, 1.0), 1.0, 0.25),
("SCHAEFFLER_010103_Steel-Galvanized", (0.65, 0.67, 0.70, 1.0), 1.0, 0.40),
("SCHAEFFLER_010104_Steel-Casted", (0.35, 0.33, 0.31, 1.0), 1.0, 0.60),
("SCHAEFFLER_010105_Steel-Plate", (0.50, 0.51, 0.53, 1.0), 1.0, 0.30),
("SCHAEFFLER_010201_Niro", (0.70, 0.72, 0.74, 1.0), 1.0, 0.20),
("SCHAEFFLER_010301_Tin", (0.75, 0.75, 0.73, 1.0), 1.0, 0.30),
("SCHAEFFLER_010401_Aluminium", (0.80, 0.80, 0.82, 1.0), 1.0, 0.25),
("SCHAEFFLER_010501_Brass", (0.70, 0.55, 0.20, 1.0), 1.0, 0.25),
("SCHAEFFLER_010601_Bronze", (0.55, 0.35, 0.15, 1.0), 1.0, 0.30),
# --- 02 Coatings ---
("SCHAEFFLER_020101_Durotect-Blue", (0.15, 0.25, 0.50, 1.0), 0.8, 0.20),
("SCHAEFFLER_020102_Durotect-Black", (0.05, 0.05, 0.06, 1.0), 0.8, 0.15),
("SCHAEFFLER_020201_Coat-Black", (0.03, 0.03, 0.03, 1.0), 0.6, 0.10),
# --- 03 Non-metals ---
("SCHAEFFLER_030101_Elastomer-Brown", (0.30, 0.18, 0.08, 1.0), 0.0, 0.55),
("SCHAEFFLER_030102_Elastomer-Green", (0.10, 0.30, 0.10, 1.0), 0.0, 0.55),
("SCHAEFFLER_030103_Elastomer-Black", (0.04, 0.04, 0.04, 1.0), 0.0, 0.55),
("SCHAEFFLER_030201_Plastic-Brown", (0.35, 0.22, 0.10, 1.0), 0.0, 0.40),
("SCHAEFFLER_030202_Plastic-Green", (0.08, 0.35, 0.12, 1.0), 0.0, 0.40),
("SCHAEFFLER_030203_Plastic-Black", (0.02, 0.02, 0.02, 1.0), 0.0, 0.40),
("SCHAEFFLER_030204_Plastic-Blue", (0.10, 0.20, 0.50, 1.0), 0.0, 0.40),
("SCHAEFFLER_030205_Plastic-White", (0.85, 0.85, 0.85, 1.0), 0.0, 0.40),
("SCHAEFFLER_030301_Plastic-Clear", (0.90, 0.90, 0.92, 1.0), 0.0, 0.10), # + transmission
("SCHAEFFLER_030302_Plastic-Translucent-White", (0.80, 0.80, 0.82, 1.0), 0.0, 0.20), # + transmission
("SCHAEFFLER_030401_TPU-Blue", (0.12, 0.25, 0.55, 1.0), 0.0, 0.45),
("SCHAEFFLER_030501_Ceramic-Black", (0.03, 0.03, 0.04, 1.0), 0.0, 0.15),
# --- 04 Compounds ---
("SCHAEFFLER_040101_E40", (0.25, 0.22, 0.18, 1.0), 0.0, 0.50),
("SCHAEFFLER_040102_E50", (0.28, 0.25, 0.20, 1.0), 0.0, 0.50),
("SCHAEFFLER_040201_Elgoglide", (0.20, 0.22, 0.25, 1.0), 0.0, 0.35),
("SCHAEFFLER_040202_Elgotex", (0.05, 0.05, 0.06, 1.0), 0.0, 0.35),
("SCHAEFFLER_040301_PTFE-Niro-Compound", (0.60, 0.62, 0.65, 1.0), 0.3, 0.25),
("SCHAEFFLER_040302_PTFE-Foil", (0.85, 0.85, 0.82, 1.0), 0.0, 0.15),
("SCHAEFFLER_040303_PTFE-Compound-Black", (0.04, 0.04, 0.05, 1.0), 0.0, 0.30),
("SCHAEFFLER_040304_PTFE-Compound-Orange", (0.70, 0.35, 0.08, 1.0), 0.0, 0.30),
("SCHAEFFLER_040305_GFK-PTFE-Compound", (0.08, 0.10, 0.08, 1.0), 0.0, 0.45),
# --- 05 Misc ---
("SCHAEFFLER_059999_FailedMaterial", (1.00, 0.00, 0.50, 1.0), 0.0, 0.50),
]
# Translucent materials that need transmission
TRANSLUCENT = {
"SCHAEFFLER_030301_Plastic-Clear": 0.9,
"SCHAEFFLER_030302_Plastic-Translucent-White": 0.5,
}
def main():
# Start from factory defaults
bpy.ops.wm.read_factory_settings(use_empty=True)
for name, color, metallic, roughness in MATERIALS:
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
# Clear default nodes
for n in nodes:
nodes.remove(n)
# Create Principled BSDF + Material Output
bsdf = nodes.new("ShaderNodeBsdfPrincipled")
bsdf.location = (0, 0)
output = nodes.new("ShaderNodeOutputMaterial")
output.location = (300, 0)
links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
# Set properties
bsdf.inputs["Base Color"].default_value = color
bsdf.inputs["Metallic"].default_value = metallic
bsdf.inputs["Roughness"].default_value = roughness
# Transmission for translucent materials
if name in TRANSLUCENT:
bsdf.inputs["Transmission Weight"].default_value = TRANSLUCENT[name]
bsdf.inputs["IOR"].default_value = 1.45
# Also set the viewport display color for solid-view preview
mat.diffuse_color = color
# Fake user so Blender keeps the material on save
mat.use_fake_user = True
# Save
out_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "material_library.blend")
bpy.ops.wm.save_mainfile(filepath=out_path)
print(f"\nSaved {len(MATERIALS)} materials to: {out_path}")
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
FROM python:3.11-slim
WORKDIR /app
# System dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Docker SDK (for dynamic flamenco-worker scaling via /var/run/docker.sock)
RUN pip install --no-cache-dir "docker>=6.1.0"
# Install Python dependencies
COPY pyproject.toml .
RUN pip install --no-cache-dir -e .
# Copy app code
COPY . .
# Create upload dirs
RUN mkdir -p uploads/step_files uploads/excel_files uploads/thumbnails
COPY start.sh /start.sh
RUN chmod +x /start.sh
EXPOSE 8000
+41
View File
@@ -0,0 +1,41 @@
[alembic]
script_location = alembic
prepend_sys_path = .
version_path_separator = os
sqlalchemy.url = postgresql://schaeffler:schaeffler@localhost:5432/schaeffler
[post_write_hooks]
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
Binary file not shown.
+62
View File
@@ -0,0 +1,62 @@
import asyncio
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from app.database import Base
from app.config import settings
# Import all models to register them with Base
import app.models # noqa: F401
config = context.config
config.set_main_option("sqlalchemy.url", settings.database_url)
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Connection) -> None:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_online() -> None:
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+26
View File
@@ -0,0 +1,26 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
@@ -0,0 +1,153 @@
"""initial schema
Revision ID: 001
Revises:
Create Date: 2026-03-01
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = "001"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# users
op.create_table(
"users",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("email", sa.String(255), nullable=False, unique=True),
sa.Column("password_hash", sa.String(255), nullable=False),
sa.Column("full_name", sa.String(255), nullable=False),
sa.Column("role", sa.Enum("admin", "client", name="userrole"), nullable=False, server_default="client"),
sa.Column("is_active", sa.Boolean, nullable=False, server_default="true"),
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
)
op.create_index("ix_users_email", "users", ["email"])
# templates
op.create_table(
"templates",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("category_key", sa.String(100), nullable=False, unique=True),
sa.Column("standard_fields", postgresql.JSONB, nullable=False, server_default="{}"),
sa.Column("component_schema", postgresql.JSONB, nullable=False, server_default="{}"),
sa.Column("description", sa.Text, nullable=True),
sa.Column("is_active", sa.Boolean, nullable=False, server_default="true"),
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
)
op.create_index("ix_templates_category_key", "templates", ["category_key"])
# cad_files
op.create_table(
"cad_files",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("original_name", sa.String(500), nullable=False),
sa.Column("stored_path", sa.String(1000), nullable=False),
sa.Column("file_hash", sa.String(64), nullable=False, unique=True),
sa.Column("file_size", sa.BigInteger, nullable=True),
sa.Column("parsed_objects", postgresql.JSONB, nullable=True),
sa.Column("thumbnail_path", sa.String(1000), nullable=True),
sa.Column("gltf_path", sa.String(1000), nullable=True),
sa.Column(
"processing_status",
sa.Enum("pending", "processing", "completed", "failed", name="processingstatus"),
nullable=False,
server_default="pending",
),
sa.Column("error_message", sa.String(2000), nullable=True),
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
)
op.create_index("ix_cad_files_file_hash", "cad_files", ["file_hash"])
# orders
op.create_table(
"orders",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("order_number", sa.String(50), nullable=False, unique=True),
sa.Column("template_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("templates.id"), nullable=True),
sa.Column(
"status",
sa.Enum("draft", "submitted", "processing", "completed", "rejected", name="orderstatus"),
nullable=False,
server_default="draft",
),
sa.Column("created_by", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=False),
sa.Column("source_excel", sa.String(1000), nullable=True),
sa.Column("notes", sa.Text, nullable=True),
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
)
op.create_index("ix_orders_order_number", "orders", ["order_number"])
# order_items
op.create_table(
"order_items",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("order_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("orders.id"), nullable=False),
sa.Column("row_index", sa.Integer, nullable=False),
sa.Column("ebene1", sa.String(500), nullable=True),
sa.Column("ebene2", sa.String(500), nullable=True),
sa.Column("baureihe", sa.String(500), nullable=True),
sa.Column("pim_id", sa.String(500), nullable=True),
sa.Column("produkt_baureihe", sa.String(500), nullable=True),
sa.Column("gewaehltes_produkt", sa.String(500), nullable=True),
sa.Column("name_cad_modell", sa.String(500), nullable=True),
sa.Column("gewuenschte_bildnummer", sa.String(500), nullable=True),
sa.Column("lagertyp", sa.String(500), nullable=True),
sa.Column("medias_rendering", sa.Boolean, nullable=True),
sa.Column("components", postgresql.JSONB, nullable=False, server_default="[]"),
sa.Column("cad_file_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("cad_files.id"), nullable=True),
sa.Column("thumbnail_path", sa.String(1000), nullable=True),
sa.Column(
"ai_validation_status",
sa.Enum("not_started", "pending", "completed", "failed", name="aivalidationstatus"),
nullable=False,
server_default="not_started",
),
sa.Column("ai_validation_result", postgresql.JSONB, nullable=True),
sa.Column(
"item_status",
sa.Enum("pending", "approved", "rejected", name="itemstatus"),
nullable=False,
server_default="pending",
),
sa.Column("notes", sa.Text, nullable=True),
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
)
# audit_log
op.create_table(
"audit_log",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=True),
sa.Column("action", sa.String(100), nullable=False),
sa.Column("entity_type", sa.String(100), nullable=True),
sa.Column("entity_id", sa.String(255), nullable=True),
sa.Column("details", postgresql.JSONB, nullable=True),
sa.Column("timestamp", sa.DateTime, nullable=False, server_default=sa.func.now()),
)
def downgrade() -> None:
op.drop_table("audit_log")
op.drop_table("order_items")
op.drop_table("orders")
op.drop_table("cad_files")
op.drop_table("templates")
op.drop_table("users")
op.execute("DROP TYPE IF EXISTS userrole")
op.execute("DROP TYPE IF EXISTS orderstatus")
op.execute("DROP TYPE IF EXISTS processingstatus")
op.execute("DROP TYPE IF EXISTS aivalidationstatus")
op.execute("DROP TYPE IF EXISTS itemstatus")
@@ -0,0 +1,33 @@
"""system settings table
Revision ID: 002
Revises: 001
Create Date: 2026-03-01
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "002"
down_revision: Union[str, None] = "001"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"system_settings",
sa.Column("key", sa.String(100), primary_key=True),
sa.Column("value", sa.Text(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True, server_default=sa.func.now()),
)
# Insert defaults
op.execute(
"INSERT INTO system_settings (key, value, updated_at) VALUES "
"('thumbnail_renderer', 'pillow', NOW())"
)
def downgrade() -> None:
op.drop_table("system_settings")
@@ -0,0 +1,31 @@
"""blender render settings
Revision ID: 003
Revises: 002
Create Date: 2026-03-01
"""
from typing import Sequence, Union
from alembic import op
revision: str = "003"
down_revision: Union[str, None] = "002"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute(
"INSERT INTO system_settings (key, value, updated_at) VALUES "
"('blender_engine', 'cycles', NOW()),"
"('blender_cycles_samples', '256', NOW()),"
"('blender_eevee_samples', '64', NOW()) "
"ON CONFLICT (key) DO NOTHING"
)
def downgrade() -> None:
op.execute(
"DELETE FROM system_settings WHERE key IN "
"('blender_engine', 'blender_cycles_samples', 'blender_eevee_samples')"
)
@@ -0,0 +1,28 @@
"""threejs render size setting
Revision ID: 004
Revises: 003
Create Date: 2026-03-01
"""
from typing import Sequence, Union
from alembic import op
revision: str = "004"
down_revision: Union[str, None] = "003"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute(
"INSERT INTO system_settings (key, value, updated_at) VALUES "
"('threejs_render_size', '512', NOW()) "
"ON CONFLICT (key) DO NOTHING"
)
def downgrade() -> None:
op.execute(
"DELETE FROM system_settings WHERE key = 'threejs_render_size'"
)
@@ -0,0 +1,28 @@
"""set threejs_render_size default to 1024
Revision ID: 005
Revises: 004
Create Date: 2026-03-01
"""
from typing import Sequence, Union
from alembic import op
revision: str = "005"
down_revision: Union[str, None] = "004"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute(
"UPDATE system_settings SET value = '1024', updated_at = NOW() "
"WHERE key = 'threejs_render_size' AND value = '512'"
)
def downgrade() -> None:
op.execute(
"UPDATE system_settings SET value = '512', updated_at = NOW() "
"WHERE key = 'threejs_render_size' AND value = '1024'"
)
@@ -0,0 +1,28 @@
"""thumbnail format setting (jpg | png)
Revision ID: 006
Revises: 005
Create Date: 2026-03-01
"""
from typing import Sequence, Union
from alembic import op
revision: str = "006"
down_revision: Union[str, None] = "005"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute(
"INSERT INTO system_settings (key, value, updated_at) VALUES "
"('thumbnail_format', 'jpg', NOW()) "
"ON CONFLICT (key) DO NOTHING"
)
def downgrade() -> None:
op.execute(
"DELETE FROM system_settings WHERE key = 'thumbnail_format'"
)
+33
View File
@@ -0,0 +1,33 @@
"""materials table and cad_part_materials column
Revision ID: 007
Revises: 006
Create Date: 2026-03-01
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID, JSONB
revision: str = "007"
down_revision: Union[str, None] = "006"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"materials",
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("name", sa.String(200), nullable=False, unique=True),
sa.Column("description", sa.Text, nullable=True),
sa.Column("created_at", sa.DateTime, server_default=sa.text("NOW()"), nullable=False),
sa.Column("updated_at", sa.DateTime, server_default=sa.text("NOW()"), nullable=False),
)
op.execute("ALTER TABLE order_items ADD COLUMN IF NOT EXISTS cad_part_materials JSONB NOT NULL DEFAULT '[]'::jsonb")
def downgrade() -> None:
op.execute("ALTER TABLE order_items DROP COLUMN IF EXISTS cad_part_materials")
op.drop_table("materials")
@@ -0,0 +1,32 @@
"""Add created_by and source to materials
Revision ID: 008
Revises: 007
Create Date: 2026-03-01
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID
revision: str = "008"
down_revision: Union[str, None] = "007"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute(
"ALTER TABLE materials "
"ADD COLUMN IF NOT EXISTS created_by UUID REFERENCES users(id) ON DELETE SET NULL, "
"ADD COLUMN IF NOT EXISTS source VARCHAR(20) NOT NULL DEFAULT 'manual'"
)
def downgrade() -> None:
op.execute(
"ALTER TABLE materials "
"DROP COLUMN IF EXISTS created_by, "
"DROP COLUMN IF EXISTS source"
)
@@ -0,0 +1,28 @@
"""Add render_log JSONB column to cad_files
Revision ID: 009
Revises: 008
Create Date: 2026-03-01
"""
from typing import Sequence, Union
from alembic import op
revision: str = "009"
down_revision: Union[str, None] = "008"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute(
"ALTER TABLE cad_files "
"ADD COLUMN IF NOT EXISTS render_log JSONB"
)
def downgrade() -> None:
op.execute(
"ALTER TABLE cad_files "
"DROP COLUMN IF EXISTS render_log"
)
@@ -0,0 +1,75 @@
"""KPI analytics and pricing tiers
Revision ID: 010
Revises: 009
Create Date: 2026-03-02
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "010"
down_revision: Union[str, None] = "009"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add project_manager to userrole enum — must be outside a transaction
op.execute("COMMIT")
op.execute("ALTER TYPE userrole ADD VALUE IF NOT EXISTS 'project_manager'")
op.execute("BEGIN")
# Lifecycle timestamps + estimated_price on orders
op.execute(
"ALTER TABLE orders "
"ADD COLUMN IF NOT EXISTS submitted_at TIMESTAMP WITHOUT TIME ZONE"
)
op.execute(
"ALTER TABLE orders "
"ADD COLUMN IF NOT EXISTS processing_started_at TIMESTAMP WITHOUT TIME ZONE"
)
op.execute(
"ALTER TABLE orders "
"ADD COLUMN IF NOT EXISTS completed_at TIMESTAMP WITHOUT TIME ZONE"
)
op.execute(
"ALTER TABLE orders "
"ADD COLUMN IF NOT EXISTS rejected_at TIMESTAMP WITHOUT TIME ZONE"
)
op.execute(
"ALTER TABLE orders "
"ADD COLUMN IF NOT EXISTS estimated_price NUMERIC(12, 2)"
)
# pricing_tiers table
op.execute(
"""
CREATE TABLE IF NOT EXISTS pricing_tiers (
id SERIAL PRIMARY KEY,
category_key VARCHAR(100) NOT NULL,
quality_level VARCHAR(50) NOT NULL DEFAULT 'Normal',
price_per_item NUMERIC(10, 2) NOT NULL,
description TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT uq_pricing_tier UNIQUE (category_key, quality_level)
)
"""
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_pricing_tiers_category_key "
"ON pricing_tiers (category_key)"
)
def downgrade() -> None:
op.execute("DROP TABLE IF EXISTS pricing_tiers")
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS estimated_price")
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS rejected_at")
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS completed_at")
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS processing_started_at")
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS submitted_at")
# Note: removing enum values is not supported in PostgreSQL without full recreation
@@ -0,0 +1,101 @@
"""Product library — products, output_types, order_lines tables
Revision ID: 011
Revises: 010
Create Date: 2026-03-02
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "011"
down_revision: Union[str, None] = "010"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute(
"""
CREATE TABLE IF NOT EXISTS products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pim_id VARCHAR(500) UNIQUE NOT NULL,
name VARCHAR(500),
category_key VARCHAR(100),
ebene1 VARCHAR(500),
ebene2 VARCHAR(500),
baureihe VARCHAR(500),
produkt_baureihe VARCHAR(500),
lagertyp VARCHAR(500),
name_cad_modell VARCHAR(500),
components JSONB NOT NULL DEFAULT '[]',
cad_part_materials JSONB NOT NULL DEFAULT '[]',
cad_file_id UUID REFERENCES cad_files(id) ON DELETE SET NULL,
notes TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
source_excel VARCHAR(1000),
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
)
"""
)
op.execute("CREATE INDEX IF NOT EXISTS ix_products_category_key ON products (category_key)")
op.execute("CREATE INDEX IF NOT EXISTS ix_products_name_cad_modell ON products (name_cad_modell)")
op.execute(
"""
CREATE TABLE IF NOT EXISTS output_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) UNIQUE NOT NULL,
description TEXT,
renderer VARCHAR(50) NOT NULL DEFAULT 'threejs',
render_settings JSONB NOT NULL DEFAULT '{}',
output_format VARCHAR(20) NOT NULL DEFAULT 'png',
sort_order INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
)
"""
)
op.execute(
"""
CREATE TABLE IF NOT EXISTS order_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES products(id),
output_type_id UUID REFERENCES output_types(id),
gewuenschte_bildnummer VARCHAR(500),
item_status VARCHAR(20) NOT NULL DEFAULT 'pending',
render_status VARCHAR(20) NOT NULL DEFAULT 'pending',
result_path VARCHAR(1000),
render_log JSONB,
ai_validation_status VARCHAR(20) NOT NULL DEFAULT 'not_started',
ai_validation_result JSONB,
notes TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
)
"""
)
op.execute("CREATE INDEX IF NOT EXISTS ix_order_lines_order_id ON order_lines (order_id)")
op.execute("CREATE INDEX IF NOT EXISTS ix_order_lines_product_id ON order_lines (product_id)")
# Partial unique indexes to handle NULL output_type_id correctly
op.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS uq_order_lines_tracking "
"ON order_lines (order_id, product_id) "
"WHERE output_type_id IS NULL"
)
op.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS uq_order_lines_render "
"ON order_lines (order_id, product_id, output_type_id) "
"WHERE output_type_id IS NOT NULL"
)
def downgrade() -> None:
op.execute("DROP TABLE IF EXISTS order_lines")
op.execute("DROP TABLE IF EXISTS output_types")
op.execute("DROP TABLE IF EXISTS products")
@@ -0,0 +1,123 @@
"""Backfill products and order_lines from order_items
Revision ID: 012
Revises: 011
Create Date: 2026-03-02
"""
from typing import Sequence, Union
from alembic import op
revision: str = "012"
down_revision: Union[str, None] = "011"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 1. Seed default output type
op.execute(
"""
INSERT INTO output_types (id, name, renderer, output_format, sort_order)
VALUES (gen_random_uuid(), '3D Thumbnail', 'threejs', 'png', 0)
ON CONFLICT (name) DO NOTHING
"""
)
# 2. Create products from distinct pim_id in order_items
# For each distinct pim_id, take fields from the most recently updated row
op.execute(
"""
INSERT INTO products (
pim_id, name, category_key, ebene1, ebene2, baureihe,
produkt_baureihe, lagertyp, name_cad_modell,
components, cad_part_materials, cad_file_id, source_excel
)
SELECT DISTINCT ON (oi.pim_id)
oi.pim_id,
oi.gewaehltes_produkt AS name,
t.category_key,
oi.ebene1,
oi.ebene2,
oi.baureihe,
oi.produkt_baureihe,
oi.lagertyp,
oi.name_cad_modell,
oi.components,
COALESCE(oi.cad_part_materials, '[]'::jsonb) AS cad_part_materials,
oi.cad_file_id,
o.source_excel
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
LEFT JOIN templates t ON t.id = o.template_id
WHERE oi.pim_id IS NOT NULL
AND oi.pim_id <> ''
ORDER BY oi.pim_id, oi.updated_at DESC
ON CONFLICT (pim_id) DO NOTHING
"""
)
# 3. Create order_lines from order_items where pim_id IS NOT NULL
# 3a. Rows with medias_rendering = true → link to '3D Thumbnail' output type
op.execute(
"""
INSERT INTO order_lines (
order_id, product_id, output_type_id,
gewuenschte_bildnummer, item_status, render_status,
ai_validation_status, ai_validation_result, notes
)
SELECT
oi.order_id,
p.id AS product_id,
ot.id AS output_type_id,
oi.gewuenschte_bildnummer,
oi.item_status::TEXT,
CASE
WHEN oi.cad_file_id IS NOT NULL THEN 'pending'
ELSE 'pending'
END AS render_status,
oi.ai_validation_status::TEXT,
oi.ai_validation_result,
oi.notes
FROM order_items oi
JOIN products p ON p.pim_id = oi.pim_id
JOIN output_types ot ON ot.name = '3D Thumbnail'
WHERE oi.pim_id IS NOT NULL
AND oi.pim_id <> ''
AND oi.medias_rendering = TRUE
ON CONFLICT DO NOTHING
"""
)
# 3b. Rows with medias_rendering = false → tracking only (no output_type_id)
op.execute(
"""
INSERT INTO order_lines (
order_id, product_id, output_type_id,
gewuenschte_bildnummer, item_status, render_status,
ai_validation_status, ai_validation_result, notes
)
SELECT
oi.order_id,
p.id AS product_id,
NULL AS output_type_id,
oi.gewuenschte_bildnummer,
oi.item_status::TEXT,
'pending' AS render_status,
oi.ai_validation_status::TEXT,
oi.ai_validation_result,
oi.notes
FROM order_items oi
JOIN products p ON p.pim_id = oi.pim_id
WHERE oi.pim_id IS NOT NULL
AND oi.pim_id <> ''
AND (oi.medias_rendering IS NULL OR oi.medias_rendering = FALSE)
ON CONFLICT DO NOTHING
"""
)
def downgrade() -> None:
op.execute("DELETE FROM order_lines")
op.execute("DELETE FROM products")
op.execute("DELETE FROM output_types WHERE name = '3D Thumbnail'")
@@ -0,0 +1,44 @@
"""Add gewuenschte_bildnummer and medias_rendering to products
Revision ID: 013
Revises: 012
Create Date: 2026-03-02
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "013"
down_revision: Union[str, None] = "012"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column("products", sa.Column("gewuenschte_bildnummer", sa.String(500), nullable=True))
op.add_column("products", sa.Column("medias_rendering", sa.Boolean(), nullable=True))
# Backfill from order_items where available
op.execute(
"""
UPDATE products p
SET gewuenschte_bildnummer = sub.gewuenschte_bildnummer,
medias_rendering = sub.medias_rendering
FROM (
SELECT DISTINCT ON (oi.pim_id)
oi.pim_id,
oi.gewuenschte_bildnummer,
oi.medias_rendering
FROM order_items oi
WHERE oi.pim_id IS NOT NULL AND oi.pim_id <> ''
ORDER BY oi.pim_id, oi.updated_at DESC
) sub
WHERE p.pim_id = sub.pim_id
"""
)
def downgrade() -> None:
op.drop_column("products", "medias_rendering")
op.drop_column("products", "gewuenschte_bildnummer")
@@ -0,0 +1,27 @@
"""Add compatible_categories to output_types
Revision ID: 014
Revises: 013
Create Date: 2026-03-02
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
revision: str = "014"
down_revision: Union[str, None] = "013"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column(
"output_types",
sa.Column("compatible_categories", JSONB, server_default="[]", nullable=False),
)
def downgrade() -> None:
op.drop_column("output_types", "compatible_categories")
@@ -0,0 +1,65 @@
"""Add Flamenco render backend support
Revision ID: 015
Revises: 014
Create Date: 2026-03-02
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "015"
down_revision: Union[str, None] = "014"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# output_types: render_backend + is_animation
op.add_column(
"output_types",
sa.Column("render_backend", sa.String(20), server_default="auto", nullable=False),
)
op.add_column(
"output_types",
sa.Column("is_animation", sa.Boolean(), server_default="false", nullable=False),
)
# order_lines: flamenco tracking columns
op.add_column(
"order_lines",
sa.Column("flamenco_job_id", sa.String(100), nullable=True),
)
op.add_column(
"order_lines",
sa.Column("render_backend_used", sa.String(20), nullable=True),
)
op.add_column(
"order_lines",
sa.Column("render_started_at", sa.DateTime(), nullable=True),
)
op.add_column(
"order_lines",
sa.Column("render_completed_at", sa.DateTime(), nullable=True),
)
# Seed system settings for Flamenco
op.execute(
"INSERT INTO system_settings (key, value) VALUES "
"('render_backend', 'celery'), "
"('flamenco_manager_url', 'http://flamenco-manager:8080'), "
"('flamenco_worker_count', '1') "
"ON CONFLICT (key) DO NOTHING"
)
def downgrade() -> None:
op.drop_column("order_lines", "render_completed_at")
op.drop_column("order_lines", "render_started_at")
op.drop_column("order_lines", "render_backend_used")
op.drop_column("order_lines", "flamenco_job_id")
op.drop_column("output_types", "is_animation")
op.drop_column("output_types", "render_backend")
op.execute("DELETE FROM system_settings WHERE key IN ('render_backend', 'flamenco_manager_url', 'flamenco_worker_count')")
@@ -0,0 +1,52 @@
"""Pricing enhancements: OutputType→PricingTier link, per-line unit_price, transparent_bg, default tier
Revision ID: 016
Revises: 015
Create Date: 2026-03-02
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "016"
down_revision: Union[str, None] = "015"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# OutputType → PricingTier link
op.add_column("output_types", sa.Column("pricing_tier_id", sa.Integer(), nullable=True))
op.create_foreign_key(
"fk_output_types_pricing_tier_id",
"output_types",
"pricing_tiers",
["pricing_tier_id"],
["id"],
ondelete="SET NULL",
)
op.create_index("ix_output_types_pricing_tier_id", "output_types", ["pricing_tier_id"])
# Transparent background option for Blender PNG renders
op.add_column("output_types", sa.Column(
"transparent_bg", sa.Boolean(), nullable=False, server_default="false",
))
# Per-line price snapshot
op.add_column("order_lines", sa.Column("unit_price", sa.Numeric(10, 2), nullable=True))
# Seed global default tier (idempotent via ON CONFLICT)
op.execute("""
INSERT INTO pricing_tiers (category_key, quality_level, price_per_item, description, is_active, created_at, updated_at)
VALUES ('default', 'Normal', 25.00, 'Global fallback price', true, NOW(), NOW())
ON CONFLICT ON CONSTRAINT uq_pricing_tier DO NOTHING
""")
def downgrade() -> None:
op.drop_column("order_lines", "unit_price")
op.drop_column("output_types", "transparent_bg")
op.drop_index("ix_output_types_pricing_tier_id", table_name="output_types")
op.drop_constraint("fk_output_types_pricing_tier_id", "output_types", type_="foreignkey")
op.drop_column("output_types", "pricing_tier_id")
@@ -0,0 +1,44 @@
"""Fix stale order_lines.item_status: auto-approve lines for non-draft orders
Revision ID: 017
Revises: 016
Create Date: 2026-03-02
"""
from typing import Sequence, Union
from alembic import op
revision: str = "017"
down_revision: Union[str, None] = "016"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Order lines belonging to submitted/processing/completed orders should
# be "approved", not stuck at "pending". The new Product Library workflow
# has no per-item approval step — submission implies approval.
op.execute("""
UPDATE order_lines
SET item_status = 'approved'
WHERE item_status = 'pending'
AND order_id IN (
SELECT id FROM orders
WHERE status IN ('submitted', 'processing', 'completed')
)
""")
# Lines belonging to rejected orders should be "rejected".
op.execute("""
UPDATE order_lines
SET item_status = 'rejected'
WHERE item_status = 'pending'
AND order_id IN (
SELECT id FROM orders WHERE status = 'rejected'
)
""")
def downgrade() -> None:
# Cannot reliably revert — the original values were all "pending" anyway.
pass
@@ -0,0 +1,56 @@
"""Render templates — .blend file templates per category/output type
Revision ID: 018
Revises: 017
Create Date: 2026-03-02
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID
# revision identifiers, used by Alembic.
revision: str = "018"
down_revision: Union[str, None] = "017"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"render_templates",
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("name", sa.String(300), nullable=False),
sa.Column("category_key", sa.String(100), nullable=True),
sa.Column("output_type_id", UUID(as_uuid=True), sa.ForeignKey("output_types.id", ondelete="SET NULL"), nullable=True),
sa.Column("blend_file_path", sa.Text, nullable=False),
sa.Column("original_filename", sa.String(500), nullable=False),
sa.Column("target_collection", sa.String(200), server_default="Product", nullable=False),
sa.Column("material_replace_enabled", sa.Boolean, server_default="false", nullable=False),
sa.Column("is_active", sa.Boolean, server_default="true", nullable=False),
sa.Column("created_at", sa.DateTime, server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime, server_default=sa.text("now()"), nullable=False),
)
# Unique constraint: one active template per (category_key, output_type_id) combo
op.create_index(
"ix_render_templates_active_unique",
"render_templates",
["category_key", "output_type_id"],
unique=True,
postgresql_where=sa.text("is_active = true"),
)
# Seed material_library_path setting
op.execute(
"INSERT INTO system_settings (key, value, updated_at) "
"VALUES ('material_library_path', '', now()) "
"ON CONFLICT (key) DO NOTHING"
)
def downgrade() -> None:
op.drop_index("ix_render_templates_active_unique", table_name="render_templates")
op.drop_table("render_templates")
op.execute("DELETE FROM system_settings WHERE key = 'material_library_path'")
@@ -0,0 +1,38 @@
"""Schaeffler standard materials — add schaeffler_code column and seed 35 materials
Revision ID: 019
Revises: 018
Create Date: 2026-03-02
"""
from alembic import op
import sqlalchemy as sa
import uuid
from datetime import datetime
revision = "019"
down_revision = "018"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("materials", sa.Column("schaeffler_code", sa.Integer(), nullable=True))
from app.data.schaeffler_materials import SCHAEFFLER_MATERIALS
conn = op.get_bind()
now = datetime.utcnow().isoformat()
for mat in SCHAEFFLER_MATERIALS:
desc = mat["description"].replace("'", "''")
name = mat["name"].replace("'", "''")
conn.execute(sa.text(
f"INSERT INTO materials (id, name, description, source, schaeffler_code, created_at, updated_at) "
f"VALUES ('{uuid.uuid4()}', '{name}', '{desc}', '{mat['source']}', "
f"{mat['schaeffler_code']}, '{now}', '{now}') "
f"ON CONFLICT (name) DO NOTHING"
))
def downgrade() -> None:
op.execute("DELETE FROM materials WHERE source = 'schaeffler_standard'")
op.drop_column("materials", "schaeffler_code")
@@ -0,0 +1,99 @@
"""Material aliases — substitution/alias system for material name resolution
Revision ID: 020
Revises: 019
Create Date: 2026-03-02
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID
import uuid
from datetime import datetime
revision = "020"
down_revision = "019"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create material_aliases table
op.create_table(
"material_aliases",
sa.Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4),
sa.Column(
"material_id",
UUID(as_uuid=True),
sa.ForeignKey("materials.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("alias", sa.String(300), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
)
# Case-insensitive unique index on alias
op.create_index(
"uq_material_aliases_alias_lower",
"material_aliases",
[sa.text("lower(alias)")],
unique=True,
)
# Index on material_id for FK lookups
op.create_index(
"ix_material_aliases_material_id",
"material_aliases",
["material_id"],
)
# Seed aliases from naming_scheme.xlsx Materialmapping data
_seed_aliases()
def _seed_aliases() -> None:
from app.data.material_alias_seeds import MATERIAL_ALIAS_SEEDS
conn = op.get_bind()
for entry in MATERIAL_ALIAS_SEEDS:
material_name = entry["material_name"]
# Look up material by name
result = conn.execute(
sa.text("SELECT id FROM materials WHERE name = :name"),
{"name": material_name},
)
row = result.fetchone()
if not row:
# Material not seeded yet, skip
continue
material_id = row[0]
for alias_str in entry["aliases"]:
# Skip if alias already exists (case-insensitive)
existing = conn.execute(
sa.text("SELECT id FROM material_aliases WHERE lower(alias) = lower(:alias)"),
{"alias": alias_str},
)
if existing.fetchone():
continue
conn.execute(
sa.text(
"INSERT INTO material_aliases (id, material_id, alias, created_at) "
"VALUES (:id, :material_id, :alias, :created_at)"
),
{
"id": str(uuid.uuid4()),
"material_id": str(material_id),
"alias": alias_str,
"created_at": datetime.utcnow(),
},
)
def downgrade() -> None:
op.drop_index("ix_material_aliases_material_id", table_name="material_aliases")
op.drop_index("uq_material_aliases_alias_lower", table_name="material_aliases")
op.drop_table("material_aliases")
@@ -0,0 +1,62 @@
"""Notification center — add target_user_id, read_at, notification to audit_log
Revision ID: 021
Revises: 020
Create Date: 2026-03-03
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID
revision = "021"
down_revision = "020"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"audit_log",
sa.Column(
"target_user_id",
UUID(as_uuid=True),
sa.ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
),
)
op.add_column(
"audit_log",
sa.Column("read_at", sa.DateTime(), nullable=True),
)
op.add_column(
"audit_log",
sa.Column(
"notification",
sa.Boolean(),
nullable=False,
server_default=sa.text("false"),
),
)
# Composite index for user notification queries
op.create_index(
"ix_audit_log_target_notification",
"audit_log",
["target_user_id", "notification", "read_at"],
)
# Partial index for listing recent notifications
op.create_index(
"ix_audit_log_notification_ts",
"audit_log",
["notification", "timestamp"],
postgresql_where=sa.text("notification = true"),
)
def downgrade() -> None:
op.drop_index("ix_audit_log_notification_ts", table_name="audit_log")
op.drop_index("ix_audit_log_target_notification", table_name="audit_log")
op.drop_column("audit_log", "notification")
op.drop_column("audit_log", "read_at")
op.drop_column("audit_log", "target_user_id")
@@ -0,0 +1,87 @@
"""Product variants — per-product material variant support
Revision ID: 022
Revises: 021
Create Date: 2026-03-03
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID, JSONB
revision = "022"
down_revision = "021"
branch_labels = None
depends_on = None
def upgrade() -> None:
# --- New table: product_variants ---
op.create_table(
"product_variants",
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("product_id", UUID(as_uuid=True), sa.ForeignKey("products.id", ondelete="CASCADE"), nullable=False, index=True),
sa.Column("name", sa.String(500), nullable=False),
sa.Column("gewuenschte_bildnummer", sa.String(500), nullable=True),
sa.Column("components", JSONB, nullable=False, server_default="[]"),
sa.Column("is_default", sa.Boolean, nullable=False, server_default="false"),
sa.Column("source_excel", sa.String(1000), nullable=True),
sa.Column("created_at", sa.DateTime, server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime, server_default=sa.func.now(), nullable=False),
)
# Unique constraint: (product_id, lower(name))
op.create_index(
"uq_product_variants_product_name",
"product_variants",
[sa.text("product_id"), sa.text("lower(name)")],
unique=True,
)
# --- Alter products ---
op.add_column("products", sa.Column("arbeitspaket", sa.String(500), nullable=True))
# Drop existing unique constraint on pim_id — PIM-ID is a class-level
# identifier shared by many products so it must NOT be unique.
op.drop_constraint("products_pim_id_key", "products", type_="unique")
op.create_index(
"uq_products_produkt_baureihe",
"products",
[sa.text("lower(produkt_baureihe)")],
unique=True,
postgresql_where=sa.text("produkt_baureihe IS NOT NULL AND is_active = true"),
)
# --- Alter order_lines ---
op.add_column(
"order_lines",
sa.Column(
"variant_id",
UUID(as_uuid=True),
sa.ForeignKey("product_variants.id", ondelete="SET NULL"),
nullable=True,
),
)
# --- Backfill: create default variants for existing products ---
op.execute("""
INSERT INTO product_variants (id, product_id, name, components, is_default, source_excel, created_at, updated_at)
SELECT
gen_random_uuid(),
p.id,
COALESCE(p.name, p.pim_id),
COALESCE(p.components, '[]'::jsonb),
true,
p.source_excel,
NOW(),
NOW()
FROM products p
WHERE p.name IS NOT NULL OR p.pim_id IS NOT NULL
""")
def downgrade() -> None:
op.drop_column("order_lines", "variant_id")
op.drop_index("uq_products_produkt_baureihe", "products")
op.create_unique_constraint("products_pim_id_key", "products", ["pim_id"])
op.drop_column("products", "arbeitspaket")
op.drop_index("uq_product_variants_product_name", "product_variants")
op.drop_table("product_variants")
@@ -0,0 +1,55 @@
"""Fix order_line unique constraints to include variant_id
The old constraints (order_id, product_id) caused 409 errors when
multiple variants of the same product were added to the same order.
New constraints use COALESCE(variant_id, nil_uuid) so different
variants of the same product can coexist.
Revision ID: 023
Revises: 022
Create Date: 2026-03-03
"""
from alembic import op
revision = "023"
down_revision = "022"
branch_labels = None
depends_on = None
NIL_UUID = "00000000-0000-0000-0000-000000000000"
def upgrade() -> None:
# Drop old constraints
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
# Recreate with variant_id included (COALESCE handles NULLs)
op.execute(
"CREATE UNIQUE INDEX uq_order_lines_tracking "
f"ON order_lines (order_id, product_id, COALESCE(variant_id, '{NIL_UUID}'::uuid)) "
"WHERE output_type_id IS NULL"
)
op.execute(
"CREATE UNIQUE INDEX uq_order_lines_render "
"ON order_lines (order_id, product_id, output_type_id, "
f"COALESCE(variant_id, '{NIL_UUID}'::uuid)) "
"WHERE output_type_id IS NOT NULL"
)
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
# Restore original constraints without variant_id
op.execute(
"CREATE UNIQUE INDEX uq_order_lines_tracking "
"ON order_lines (order_id, product_id) "
"WHERE output_type_id IS NULL"
)
op.execute(
"CREATE UNIQUE INDEX uq_order_lines_render "
"ON order_lines (order_id, product_id, output_type_id) "
"WHERE output_type_id IS NOT NULL"
)
@@ -0,0 +1,33 @@
"""Add lighting_only column to render_templates
When lighting_only=True the render script uses the template's World/HDRI for
lighting but always computes an auto-camera for product framing. This is
useful for HDR-only templates that don't define a fixed camera angle.
Revision ID: 024
Revises: 023
Create Date: 2026-03-03
"""
from alembic import op
import sqlalchemy as sa
revision = '024'
down_revision = '023'
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
'render_templates',
sa.Column(
'lighting_only',
sa.Boolean(),
nullable=False,
server_default='false',
),
)
def downgrade():
op.drop_column('render_templates', 'lighting_only')
@@ -0,0 +1,24 @@
"""Add cycles_device column to output_types
Revision ID: 025
Revises: 024
Create Date: 2026-03-03
"""
from alembic import op
import sqlalchemy as sa
revision = '025'
down_revision = '024'
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
'output_types',
sa.Column('cycles_device', sa.String(10), nullable=True),
)
def downgrade():
op.drop_column('output_types', 'cycles_device')
@@ -0,0 +1,25 @@
"""Add shadow_catcher_enabled to render_templates.
Revision ID: 026
Revises: 025
Create Date: 2026-03-03
"""
from alembic import op
import sqlalchemy as sa
revision = '026'
down_revision = '025'
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
'render_templates',
sa.Column('shadow_catcher_enabled', sa.Boolean(), nullable=False,
server_default='false'),
)
def downgrade():
op.drop_column('render_templates', 'shadow_catcher_enabled')
@@ -0,0 +1,108 @@
"""Remove product variant system — products are unique, no variant concept.
Revision ID: 027
Revises: 026
Create Date: 2026-03-03
"""
from alembic import op
import sqlalchemy as sa
revision = "027"
down_revision = "026"
branch_labels = None
depends_on = None
NIL_UUID = "00000000-0000-0000-0000-000000000000"
def upgrade() -> None:
# Drop variant-aware unique indexes on order_lines
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
# Drop variant_id column from order_lines
op.execute("ALTER TABLE order_lines DROP COLUMN IF EXISTS variant_id")
# Deduplicate tracking-only lines (output_type_id IS NULL) — keep the newest row per
# (order_id, product_id) pair so the unique index can be created cleanly.
op.execute("""
DELETE FROM order_lines
WHERE output_type_id IS NULL
AND id NOT IN (
SELECT DISTINCT ON (order_id, product_id) id
FROM order_lines
WHERE output_type_id IS NULL
ORDER BY order_id, product_id, created_at DESC
)
""")
# Deduplicate render lines — keep the newest row per (order_id, product_id, output_type_id).
op.execute("""
DELETE FROM order_lines
WHERE output_type_id IS NOT NULL
AND id NOT IN (
SELECT DISTINCT ON (order_id, product_id, output_type_id) id
FROM order_lines
WHERE output_type_id IS NOT NULL
ORDER BY order_id, product_id, output_type_id, created_at DESC
)
""")
# Recreate simpler unique indexes without variant_id
op.execute(
"CREATE UNIQUE INDEX uq_order_lines_tracking "
"ON order_lines (order_id, product_id) "
"WHERE output_type_id IS NULL"
)
op.execute(
"CREATE UNIQUE INDEX uq_order_lines_render "
"ON order_lines (order_id, product_id, output_type_id) "
"WHERE output_type_id IS NOT NULL"
)
# Drop product_variants table (CASCADE removes its indexes automatically)
op.execute("DROP TABLE IF EXISTS product_variants CASCADE")
def downgrade() -> None:
# Recreate product_variants table
op.execute("""
CREATE TABLE product_variants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
name VARCHAR(500) NOT NULL,
gewuenschte_bildnummer VARCHAR(500),
components JSONB NOT NULL DEFAULT '[]',
is_default BOOLEAN NOT NULL DEFAULT false,
source_excel VARCHAR(1000),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
)
""")
op.execute(
"CREATE UNIQUE INDEX uq_product_variants_product_name "
"ON product_variants (product_id, lower(name))"
)
op.execute("CREATE INDEX ON product_variants (product_id)")
# Add back variant_id to order_lines
op.execute(
"ALTER TABLE order_lines ADD COLUMN variant_id UUID "
"REFERENCES product_variants(id) ON DELETE SET NULL"
)
# Drop simple indexes and restore variant-aware indexes
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
op.execute(
"CREATE UNIQUE INDEX uq_order_lines_tracking "
f"ON order_lines (order_id, product_id, COALESCE(variant_id, '{NIL_UUID}'::uuid)) "
"WHERE output_type_id IS NULL"
)
op.execute(
"CREATE UNIQUE INDEX uq_order_lines_render "
"ON order_lines (order_id, product_id, output_type_id, "
f"COALESCE(variant_id, '{NIL_UUID}'::uuid)) "
"WHERE output_type_id IS NOT NULL"
)
@@ -0,0 +1,81 @@
"""Add product_render_positions table and render_position_id on order_lines.
Revision ID: 028
Revises: 027
Create Date: 2026-03-04
"""
from alembic import op
import sqlalchemy as sa
revision = "028"
down_revision = "027"
branch_labels = None
depends_on = None
NIL_UUID = "00000000-0000-0000-0000-000000000000"
def upgrade() -> None:
# ── New table: product_render_positions ──────────────────────────────────
op.execute("""
CREATE TABLE product_render_positions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
name VARCHAR(200) NOT NULL,
rotation_x DOUBLE PRECISION NOT NULL DEFAULT 0,
rotation_y DOUBLE PRECISION NOT NULL DEFAULT 0,
rotation_z DOUBLE PRECISION NOT NULL DEFAULT 0,
is_default BOOLEAN NOT NULL DEFAULT false,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
)
""")
op.execute(
"CREATE UNIQUE INDEX uq_render_positions_product_name "
"ON product_render_positions (product_id, lower(name))"
)
op.execute("CREATE INDEX ix_render_positions_product_id ON product_render_positions (product_id)")
# ── Add render_position_id to order_lines ────────────────────────────────
op.execute(
"ALTER TABLE order_lines ADD COLUMN render_position_id UUID "
"REFERENCES product_render_positions(id) ON DELETE SET NULL"
)
# ── Update unique indexes to include position ─────────────────────────────
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
op.execute(
f"CREATE UNIQUE INDEX uq_order_lines_tracking "
f"ON order_lines (order_id, product_id, COALESCE(render_position_id, '{NIL_UUID}'::uuid)) "
f"WHERE output_type_id IS NULL"
)
op.execute(
f"CREATE UNIQUE INDEX uq_order_lines_render "
f"ON order_lines (order_id, product_id, output_type_id, "
f"COALESCE(render_position_id, '{NIL_UUID}'::uuid)) "
f"WHERE output_type_id IS NOT NULL"
)
def downgrade() -> None:
# Restore original unique indexes (without position)
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
op.execute("ALTER TABLE order_lines DROP COLUMN IF EXISTS render_position_id")
op.execute(
"CREATE UNIQUE INDEX uq_order_lines_tracking "
"ON order_lines (order_id, product_id) "
"WHERE output_type_id IS NULL"
)
op.execute(
"CREATE UNIQUE INDEX uq_order_lines_render "
"ON order_lines (order_id, product_id, output_type_id) "
"WHERE output_type_id IS NOT NULL"
)
op.execute("DROP TABLE IF EXISTS product_render_positions CASCADE")
@@ -0,0 +1,53 @@
"""Seed default render positions (3/4 Front + 3/4 Rear) for all existing products.
Revision ID: 029
Revises: 028
Create Date: 2026-03-04
"""
from alembic import op
revision = "029"
down_revision = "028"
branch_labels = None
depends_on = None
def upgrade():
# Insert two default positions for every product that currently has none.
# The CTE guarantees both rows are inserted for the same set of products.
op.execute("""
WITH products_without_positions AS (
SELECT p.id AS product_id
FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM product_render_positions rp WHERE rp.product_id = p.id
)
)
INSERT INTO product_render_positions
(id, product_id, name, rotation_x, rotation_y, rotation_z,
is_default, sort_order, created_at, updated_at)
SELECT gen_random_uuid(), product_id,
'3/4 Front', -15.0, 45.0, 0.0, true, 0, NOW(), NOW()
FROM products_without_positions
UNION ALL
SELECT gen_random_uuid(), product_id,
'3/4 Rear', -15.0, -135.0, 0.0, false, 1, NOW(), NOW()
FROM products_without_positions
""")
def downgrade():
# Remove positions named exactly '3/4 Front' or '3/4 Rear'
# where they are the only two positions on that product (i.e. seeded ones).
op.execute("""
DELETE FROM product_render_positions
WHERE name IN ('3/4 Front', '3/4 Rear')
AND product_id IN (
SELECT product_id
FROM product_render_positions
GROUP BY product_id
HAVING COUNT(*) = 2
AND bool_or(name = '3/4 Front')
AND bool_or(name = '3/4 Rear')
)
""")
@@ -0,0 +1,39 @@
"""Seed 'Default' (unrotated) render position for all existing products.
Revision ID: 030
Revises: 029
Create Date: 2026-03-04
"""
from alembic import op
revision = "030"
down_revision = "029"
branch_labels = None
depends_on = None
def upgrade():
# Add 'Default' (0°/0°/0°) to every product that doesn't already have it.
op.execute("""
INSERT INTO product_render_positions
(id, product_id, name, rotation_x, rotation_y, rotation_z,
is_default, sort_order, created_at, updated_at)
SELECT gen_random_uuid(), p.id,
'Default', 0.0, 0.0, 0.0, false, 2, NOW(), NOW()
FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM product_render_positions rp
WHERE rp.product_id = p.id
AND lower(rp.name) = 'default'
)
""")
def downgrade():
op.execute("""
DELETE FROM product_render_positions
WHERE lower(name) = 'default'
AND rotation_x = 0.0
AND rotation_y = 0.0
AND rotation_z = 0.0
""")
@@ -0,0 +1,25 @@
"""Add camera_orbit to render_templates
Revision ID: 031_camera_orbit
Revises: 030_seed_default_position
Create Date: 2026-03-04
"""
from alembic import op
import sqlalchemy as sa
revision = '031'
down_revision = '030'
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
'render_templates',
sa.Column('camera_orbit', sa.Boolean(), nullable=False,
server_default='true'),
)
def downgrade():
op.drop_column('render_templates', 'camera_orbit')

Some files were not shown because too many files have changed in this diff Show More