# EspoCRM Custom Actions - Implementierungsprinzip

**Version:** 1.0  
**Datum:** 24. Januar 2026  
**Blueprint für:** Custom Button Actions mit Entity-Erstellung und Relationen

---

## 📋 Inhaltsverzeichnis

1. [Überblick](#überblick)
2. [Architektur-Prinzipien](#architektur-prinzipien)
3. [Implementierungs-Schritte](#implementierungs-schritte)
4. [Code-Template](#code-template)
5. [Best Practices](#best-practices)
6. [Sicherheit](#sicherheit)
7. [Testing & Debugging](#testing--debugging)
8. [Beispiel-Implementierung](#beispiel-implementierung)

---

## Überblick

Dieses Dokument beschreibt das standardisierte Vorgehen zur Implementierung von Custom Button Actions in EspoCRM, die:
- Von einer Entity-Detailansicht ausgelöst werden
- Eine neue verwandte Entity erstellen
- Automatisch Daten und Relationen übertragen
- ACL-konform und transaktionssicher arbeiten

**Basis-Beispiel:** Button "Räumungsklage einleiten" in Mietobjekt → Erstellt CVmhRumungsklage mit allen Relationen

---

## Architektur-Prinzipien

### 1. Drei-Schichten-Architektur

```
┌─────────────────────────────────────────────────────┐
│ FRONTEND (client/custom/)                           │
│ • JavaScript Handler (Action Trigger)               │
│ • Button Definition (clientDefs)                    │
│ • i18n Translations                                 │
└────────────────┬────────────────────────────────────┘
                 │ AJAX POST Request
┌────────────────▼────────────────────────────────────┐
│ CONTROLLER (custom/Espo/Custom/Controllers/)        │
│ • Request Validation                                │
│ • Service Call                                      │
│ • Response Formatting                               │
└────────────────┬────────────────────────────────────┘
                 │ Method Call
┌────────────────▼────────────────────────────────────┐
│ SERVICE (custom/Espo/Custom/Services/)              │
│ • ACL Security Checks                               │
│ • Business Logic                                    │
│ • Transaction Management                            │
│ • Entity Operations                                 │
└─────────────────────────────────────────────────────┘
```

### 2. Separation of Concerns

| Schicht | Verantwortlichkeit | Nicht erlaubt |
|---------|-------------------|---------------|
| **Frontend** | User Interaction, Confirmation, Navigation | Business Logic, Direct DB Access |
| **Controller** | HTTP Request/Response Handling | Business Logic, Direct Entity Manipulation |
| **Service** | Business Logic, ACL, Transactions, Entity Operations | HTTP Handling, View Logic |

---

## Implementierungs-Schritte

### Schritt 1: Backend Controller erstellen

**Datei:** `custom/Espo/Custom/Controllers/[EntityName].php`

```php
<?php
namespace Espo\Custom\Controllers;

use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Api\Request;

class CYourEntity extends \Espo\Core\Templates\Controllers\Base
{
    /**
     * POST Action: Beschreibung der Aktion
     */
    public function postActionYourAction(Request $request): array
    {
        $data = $request->getParsedBody();
        
        $id = $data->id ?? null;
        if (!$id) {
            throw new BadRequest('No Entity ID provided');
        }
        
        $service = $this->getRecordService();
        $result = $service->yourAction($id);
        
        return $result;
    }
}
```

**Wichtig:**
- Methode MUSS `postAction[Name]` heißen
- Parameter ist `Request $request`
- Return Type ist `array`
- Extends `\Espo\Core\Templates\Controllers\Base`

---

### Schritt 2: Backend Service erstellen

**Datei:** `custom/Espo/Custom/Services/[EntityName].php`

```php
<?php
namespace Espo\Custom\Services;

use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;

class CYourEntity extends \Espo\Services\Record
{
    /**
     * Beschreibung der Business-Logik
     * 
     * @param string $sourceId
     * @return array
     * @throws NotFound
     * @throws Forbidden
     */
    public function yourAction(string $sourceId): array
    {
        // 1. LADEN: Source Entity laden
        $source = $this->entityManager->getEntity('CYourEntity', $sourceId);
        if (!$source) {
            throw new NotFound('Entity not found');
        }
        
        // 2. SECURITY: ACL-Checks
        if (!$this->acl->check($source, 'read')) {
            throw new Forbidden('No read access to source entity');
        }
        
        if (!$this->acl->checkScope('CTargetEntity', 'create')) {
            throw new Forbidden('No create access to target entity');
        }
        
        // 3. TRANSACTION: Start
        $this->entityManager->getTransactionManager()->start();
        
        try {
            // 4. ERSTELLEN: Target Entity
            $data = new \stdClass();
            $data->name = 'Generated from ' . $source->get('name');
            
            // Optional: User und Teams übernehmen
            if ($source->get('assignedUserId')) {
                $data->assignedUserId = $source->get('assignedUserId');
            }
            
            $teamsIds = $source->getLinkMultipleIdList('teams');
            if (!empty($teamsIds)) {
                $data->teamsIds = $teamsIds;
            }
            
            $target = $this->entityManager->createEntity('CTargetEntity', (array)$data);
            
            if (!$target) {
                throw new \RuntimeException('Failed to create target entity');
            }
            
            // 5. RELATIONEN: Verknüpfen
            $targetRepo = $this->entityManager->getRepository('CTargetEntity');
            
            // Source → Target
            $targetRepo
                ->getRelation($target, 'sources')
                ->relate($source);
            
            // Related Entities übertragen
            $relatedEntities = $this->entityManager
                ->getRepository('CYourEntity')
                ->getRelation($source, 'relatedThings')
                ->find();
            
            foreach ($relatedEntities as $related) {
                $targetRepo
                    ->getRelation($target, 'relatedThings')
                    ->relate($related);
            }
            
            // 6. COMMIT: Transaktion abschließen
            $this->entityManager->getTransactionManager()->commit();
            
            // 7. RETURN: ID und Name für Frontend
            return [
                'id' => $target->getId(),
                'name' => $target->get('name')
            ];
            
        } catch (\Exception $e) {
            // ROLLBACK: Bei jedem Fehler
            $this->entityManager->getTransactionManager()->rollback();
            throw $e;
        }
    }
}
```

**Wichtig:**
- Extends `\Espo\Services\Record`
- Immer Transactions verwenden
- Alle Exceptions im catch-Block fangen
- Rollback bei Fehlern

---

### Schritt 3: Button im Frontend konfigurieren

**Datei:** `custom/Espo/Custom/Resources/metadata/clientDefs/[EntityName].json`

```json
{
    "menu": {
        "detail": {
            "buttons": [
                {
                    "name": "initiateYourAction",
                    "label": "Your Action Button",
                    "handler": "custom:handlers/your-entity/your-action",
                    "iconHtml": "<span class=\"fas fa-bolt\"></span>",
                    "style": "danger",
                    "acl": "edit"
                }
            ]
        }
    }
}
```

**Button-Optionen:**
- `name`: Eindeutiger Identifier
- `label`: Translation-Key (siehe i18n)
- `handler`: Pfad zum JavaScript Handler
- `iconHtml`: FontAwesome Icon oder HTML
- `style`: `default`, `success`, `danger`, `warning`, `info`
- `acl`: `read`, `edit`, `delete` (benötigte Permission)

---

### Schritt 4: JavaScript Handler implementieren

**Datei:** `client/custom/src/handlers/your-entity/your-action.js`

```javascript
define('custom:handlers/your-entity/your-action', [], function () {
    
    class YourActionHandler {
        
        constructor(view) {
            this.view = view;
        }
        
        /**
         * Setup-Methode wird automatisch aufgerufen
         */
        initYourAction() {
            this.view.listenTo(
                this.view, 
                'after:render', 
                () => this.controlButtonVisibility()
            );
        }
        
        /**
         * Button-Sichtbarkeit steuern (optional)
         */
        controlButtonVisibility() {
            const model = this.view.model;
            
            // Beispiel: Button nur bei bestimmten Bedingungen zeigen
            if (!model.get('someField')) {
                this.view.hideHeaderActionItem('initiateYourAction');
            } else {
                this.view.showHeaderActionItem('initiateYourAction');
            }
        }
        
        /**
         * Action-Handler (wird bei Button-Click aufgerufen)
         */
        actionInitiateYourAction() {
            const model = this.view.model;
            
            // 1. CONFIRMATION: User bestätigen lassen
            this.view.confirm(
                this.view.translate('confirmationMessage', 'messages', 'CYourEntity'),
                () => {
                    this.initiateAction(model.id);
                }
            );
        }
        
        /**
         * AJAX Request zum Backend
         */
        initiateAction(entityId) {
            Espo.Ui.notify(this.view.translate('pleaseWait', 'messages'));
            
            Espo.Ajax.postRequest('CYourEntity/action/yourAction', {
                id: entityId
            })
            .then(response => {
                Espo.Ui.success(
                    this.view.translate('successMessage', 'messages', 'CYourEntity')
                );
                
                // Navigation zur erstellten Entity
                this.view.getRouter().navigate(
                    '#CTargetEntity/view/' + response.id, 
                    {trigger: true}
                );
            })
            .catch(xhr => {
                console.error('Action failed:', xhr);
                
                let errorMessage = this.view.translate('errorMessage', 'messages', 'CYourEntity');
                
                if (xhr.status === 403) {
                    errorMessage = this.view.translate('Access denied', 'messages');
                } else if (xhr.status === 404) {
                    errorMessage = this.view.translate('Not found', 'messages');
                }
                
                Espo.Ui.error(errorMessage);
            });
        }
    }
    
    return YourActionHandler;
});
```

**Wichtig:**
- Class-Name muss mit Filename übereinstimmen (CamelCase)
- Methode `init[ButtonName]()` für Setup
- Methode `action[ButtonName]()` für Click-Handler
- Immer Confirmation Dialog für kritische Actions
- Error Handling mit spezifischen HTTP-Status-Codes

---

### Schritt 5: Übersetzungen hinzufügen

**Datei:** `custom/Espo/Custom/Resources/i18n/de_DE/[EntityName].json`

```json
{
    "labels": {
        "Your Action Button": "Ihre Aktion"
    },
    "messages": {
        "confirmationMessage": "Möchten Sie diese Aktion wirklich ausführen?",
        "successMessage": "Die Aktion wurde erfolgreich ausgeführt",
        "errorMessage": "Fehler beim Ausführen der Aktion"
    }
}
```

**Datei:** `custom/Espo/Custom/Resources/i18n/en_US/[EntityName].json`

```json
{
    "labels": {
        "Your Action Button": "Your Action"
    },
    "messages": {
        "confirmationMessage": "Do you really want to execute this action?",
        "successMessage": "Action executed successfully",
        "errorMessage": "Error executing action"
    }
}
```

---

### Schritt 6: Validierung & Rebuild

```bash
# Im Root-Verzeichnis von EspoCRM
python3 custom/scripts/validate_and_rebuild.py
```

**Prüfungen:**
- ✅ JSON-Syntax aller Dateien
- ✅ PHP-Syntax (Service, Controller)
- ✅ JavaScript-Syntax (Handler)
- ✅ Relationship-Konsistenz
- ✅ Automatischer Cache-Clear & Rebuild

---

## Best Practices

### 1. Naming Conventions

| Element | Pattern | Beispiel |
|---------|---------|----------|
| Controller Action | `postAction[Name]` | `postActionInitiateEviction` |
| Service Method | `camelCase` | `initiateEviction` |
| Button Name | `camelCase` | `initiateEviction` |
| JS Handler File | `kebab-case.js` | `eviction-action.js` |
| JS Class Name | `PascalCase` | `EvictionActionHandler` |
| JS Init Method | `init[ButtonName]` | `initInitiateEviction` |
| JS Action Method | `action[ButtonName]` | `actionInitiateEviction` |

### 2. Transaction Management

**Immer Transactions verwenden** bei:
- Entity-Erstellung
- Mehreren Relationierungen
- Datenänderungen über mehrere Entities

```php
// ✅ RICHTIG
$this->entityManager->getTransactionManager()->start();
try {
    // ... operations
    $this->entityManager->getTransactionManager()->commit();
} catch (\Exception $e) {
    $this->entityManager->getTransactionManager()->rollback();
    throw $e;
}

// ❌ FALSCH - Keine Transaction
$entity = $this->entityManager->createEntity(...);
// Bei Fehler in nächstem Schritt: Inkonsistente Daten!
$this->entityManager->getRepository(...)->relate(...);
```

### 3. Repository Caching

```php
// ✅ RICHTIG - Repository nur 1× holen
$targetRepo = $this->entityManager->getRepository('Target');
foreach ($items as $item) {
    $targetRepo->getRelation($target, 'items')->relate($item);
}

// ❌ FALSCH - Repository in Schleife holen
foreach ($items as $item) {
    $this->entityManager->getRepository('Target')
        ->getRelation($target, 'items')
        ->relate($item);
}
```

### 4. Error Messages

```php
// ✅ RICHTIG - Spezifische Exceptions
if (!$source) {
    throw new NotFound('Mietobjekt not found');
}
if (!$this->acl->check($source, 'read')) {
    throw new Forbidden('No read access to Mietobjekt');
}

// ❌ FALSCH - Generische Fehler
if (!$source) {
    throw new \Exception('Error');
}
```

### 5. Null Checks

```php
// ✅ RICHTIG
$entity = $this->entityManager->createEntity(...);
if (!$entity) {
    throw new \RuntimeException('Failed to create entity');
}

// ❌ FALSCH - Keine Prüfung
$entity = $this->entityManager->createEntity(...);
$entity->getId(); // Kann zu Fehler führen!
```

---

## Sicherheit

### 1. ACL-Checks (Access Control Lists)

**Minimum:** Immer beide Checks durchführen:

```php
// Check 1: Kann User Source-Entity lesen?
if (!$this->acl->check($sourceEntity, 'read')) {
    throw new Forbidden('No read access');
}

// Check 2: Kann User Target-Entity erstellen?
if (!$this->acl->checkScope('TargetEntity', 'create')) {
    throw new Forbidden('No create access');
}
```

**Optional:** Zusätzliche Checks je nach Business-Logik:

```php
// Check 3: Kann User Source-Entity bearbeiten?
if (!$this->acl->check($sourceEntity, 'edit')) {
    throw new Forbidden('No edit access');
}

// Check 4: Ownership-Check
if ($sourceEntity->get('assignedUserId') !== $this->user->getId()) {
    throw new Forbidden('Not the owner');
}
```

### 2. Input Validation

```php
// ✅ RICHTIG - ID validieren
$id = $data->id ?? null;
if (!$id || !is_string($id)) {
    throw new BadRequest('Invalid ID provided');
}

// Optional: Format prüfen
if (!preg_match('/^[a-f0-9]{17}$/', $id)) {
    throw new BadRequest('Invalid ID format');
}
```

### 3. SQL Injection Prevention

**Automatisch geschützt** durch:
- ✅ EntityManager ORM (immer verwenden!)
- ✅ Repository Pattern
- ✅ Prepared Statements

**Niemals:**
- ❌ Direct SQL Queries
- ❌ String Concatenation in Queries
- ❌ `$pdo->query()` direkt verwenden

### 4. XSS Prevention

**Automatisch geschützt** durch:
- ✅ EspoCRM Template Engine
- ✅ Automatic HTML Escaping
- ✅ Content Security Policy Headers

**Im Frontend:**
```javascript
// ✅ RICHTIG - translate() escaped automatisch
this.view.translate('message', 'messages', 'Entity');

// ❌ FALSCH - Rohe HTML-Injection
$('.container').html('<div>' + userInput + '</div>');
```

### 5. CSRF Protection

**Automatisch geschützt** durch:
- ✅ EspoCRM API Token System
- ✅ Espo.Ajax verwendet automatisch CSRF-Token
- ✅ Session-basierte Validierung

**Kein manueller Code nötig!**

---

## Testing & Debugging

### 1. Entwickler-Workflow

```bash
# 1. Code ändern
vim custom/Espo/Custom/Services/YourEntity.php

# 2. Validieren & Rebuild
python3 custom/scripts/validate_and_rebuild.py

# 3. Browser Hard-Refresh
# Ctrl + Shift + R (Chrome/Firefox)
# Cmd + Shift + R (Mac)

# 4. Test durchführen

# 5. Logs prüfen bei Fehlern
tail -f data/logs/espo-$(date +%Y-%m-%d).log
```

### 2. Häufige Fehler & Lösungen

| Fehler | Ursache | Lösung |
|--------|---------|--------|
| `Error 500: Call to undefined method getAcl()` | Falsche ACL-Methode | Nutze `$this->acl` statt `$this->getAcl()` |
| `Error 500: Entity does not have a relation 'xyz'` | Relation existiert nicht | Prüfe entityDefs, korrekter Relationname |
| Button erscheint nicht | Falsche clientDefs-Struktur | Nutze `menu.detail.buttons` nicht `detailActionList` |
| JavaScript-Fehler | Handler nicht gefunden | Prüfe Pfad und Class-Name, Cache leeren |
| Transaction-Fehler | Verschachtelte Transactions | Nur eine Transaction auf Service-Level |

### 3. Debugging-Kommandos

```bash
# Logs live verfolgen
tail -f data/logs/espo-$(date +%Y-%m-%d).log

# Letzten Fehler finden
tail -n 200 data/logs/espo-$(date +%Y-%m-%d).log | grep -i "error"

# PHP-Syntax prüfen
find custom/Espo/Custom -name "*.php" -exec php -l {} \;

# JSON-Syntax prüfen
find custom/Espo/Custom/Resources/metadata -name "*.json" -exec python3 -m json.tool {} \; > /dev/null

# Cache manuell löschen
rm -rf data/cache/*
```

### 4. Browser DevTools

**Console (F12):**
```javascript
// Current Model inspizieren
console.log(this.model.attributes);

// AJAX Request debuggen
Espo.Ajax.postRequest('Entity/action/test', {id: 'xyz'})
    .then(r => console.log('Success:', r))
    .catch(e => console.error('Error:', e));
```

---

## Beispiel-Implementierung

### Use Case: "Räumungsklage einleiten"

**Requirement:**
- Button in Mietobjekt-Detailansicht
- Erstellt CVmhRumungsklage
- Verknüpft Mietverhältnisse, Vermieter (Kläger), Mieter (Beklagte)
- Kopiert assignedUser und Teams

### Dateien-Struktur

```
custom/Espo/Custom/
├── Controllers/
│   └── CMietobjekt.php              # POST Action Endpoint
├── Services/
│   └── CMietobjekt.php              # Business Logic
└── Resources/
    ├── metadata/clientDefs/
    │   └── CMietobjekt.json         # Button Definition
    └── i18n/
        ├── de_DE/
        │   └── CMietobjekt.json     # Deutsche Übersetzungen
        └── en_US/
            └── CMietobjekt.json     # Englische Übersetzungen

client/custom/src/handlers/
└── mietobjekt/
    └── eviction-action.js           # Frontend Handler
```

### Vollständiger Code

**1. Controller** ([CMietobjekt.php](custom/Espo/Custom/Controllers/CMietobjekt.php)):
```php
<?php
namespace Espo\Custom\Controllers;

use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Api\Request;

class CMietobjekt extends \Espo\Core\Templates\Controllers\Base
{
    public function postActionInitiateEviction(Request $request): array
    {
        $data = $request->getParsedBody();
        $id = $data->id ?? null;
        
        if (!$id) {
            throw new BadRequest('No Mietobjekt ID provided');
        }
        
        return $this->getRecordService()->initiateEviction($id);
    }
}
```

**2. Service** ([CMietobjekt.php](custom/Espo/Custom/Services/CMietobjekt.php)):
```php
<?php
namespace Espo\Custom\Services;

use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;

class CMietobjekt extends \Espo\Services\Record
{
    public function initiateEviction(string $mietobjektId): array
    {
        // 1. Load & Validate
        $mietobjekt = $this->entityManager->getEntity('CMietobjekt', $mietobjektId);
        if (!$mietobjekt) {
            throw new NotFound('Mietobjekt not found');
        }
        
        // 2. ACL Checks
        if (!$this->acl->check($mietobjekt, 'read')) {
            throw new Forbidden('No read access to Mietobjekt');
        }
        if (!$this->acl->checkScope('CVmhRumungsklage', 'create')) {
            throw new Forbidden('No create access to Räumungsklage');
        }
        
        // 3. Start Transaction
        $this->entityManager->getTransactionManager()->start();
        
        try {
            // 4. Create Räumungsklage
            $data = new \stdClass();
            $data->name = 'Räumungsklage - ' . $mietobjekt->get('name');
            
            if ($mietobjekt->get('assignedUserId')) {
                $data->assignedUserId = $mietobjekt->get('assignedUserId');
            }
            
            $teamsIds = $mietobjekt->getLinkMultipleIdList('teams');
            if (!empty($teamsIds)) {
                $data->teamsIds = $teamsIds;
            }
            
            $raeumungsklage = $this->entityManager->createEntity(
                'CVmhRumungsklage', 
                (array)$data
            );
            
            if (!$raeumungsklage) {
                throw new \RuntimeException('Failed to create Räumungsklage');
            }
            
            $repo = $this->entityManager->getRepository('CVmhRumungsklage');
            
            // 5. Link Relations
            $repo->getRelation($raeumungsklage, 'mietobjekte')
                ->relate($mietobjekt);
            
            $mietverhaeltnisse = $this->entityManager
                ->getRepository('CMietobjekt')
                ->getRelation($mietobjekt, 'vmhMietverhltnises')
                ->find();
            
            foreach ($mietverhaeltnisse as $mv) {
                $repo->getRelation($raeumungsklage, 'vmhMietverhltnises')
                    ->relate($mv);
                
                // Link Vermieter as Kläger
                $vermieter = $this->entityManager
                    ->getRepository('CVmhMietverhltnis')
                    ->getRelation($mv, 'vmhbeteiligtevermieter')
                    ->find();
                foreach ($vermieter as $v) {
                    $repo->getRelation($raeumungsklage, 'klaeger')->relate($v);
                }
                
                // Link Mieter as Beklagte
                $mieter = $this->entityManager
                    ->getRepository('CVmhMietverhltnis')
                    ->getRelation($mv, 'vmhbeteiligtemieter')
                    ->find();
                foreach ($mieter as $m) {
                    $repo->getRelation($raeumungsklage, 'beklagte')->relate($m);
                }
            }
            
            // 6. Commit
            $this->entityManager->getTransactionManager()->commit();
            
            return [
                'id' => $raeumungsklage->getId(),
                'name' => $raeumungsklage->get('name')
            ];
            
        } catch (\Exception $e) {
            $this->entityManager->getTransactionManager()->rollback();
            throw $e;
        }
    }
}
```

**3. Button Config** ([clientDefs/CMietobjekt.json](custom/Espo/Custom/Resources/metadata/clientDefs/CMietobjekt.json)):
```json
{
    "menu": {
        "detail": {
            "buttons": [
                {
                    "name": "initiateEviction",
                    "label": "Initiate Eviction",
                    "handler": "custom:handlers/mietobjekt/eviction-action",
                    "iconHtml": "<span class=\"fas fa-gavel\"></span>",
                    "style": "danger",
                    "acl": "edit"
                }
            ]
        }
    }
}
```

**4. JavaScript Handler** ([eviction-action.js](client/custom/src/handlers/mietobjekt/eviction-action.js)):
```javascript
define('custom:handlers/mietobjekt/eviction-action', [], function () {
    
    class EvictionActionHandler {
        
        constructor(view) {
            this.view = view;
        }
        
        initInitiateEviction() {
            // Optional: Button-Logik nach Render
        }
        
        actionInitiateEviction() {
            this.view.confirm(
                this.view.translate('confirmEviction', 'messages', 'CMietobjekt'),
                () => this.initiateEviction(this.view.model.id)
            );
        }
        
        initiateEviction(id) {
            Espo.Ui.notify(this.view.translate('pleaseWait', 'messages'));
            
            Espo.Ajax.postRequest('CMietobjekt/action/initiateEviction', { id })
                .then(response => {
                    Espo.Ui.success(
                        this.view.translate('evictionCreated', 'messages', 'CMietobjekt')
                    );
                    this.view.getRouter().navigate(
                        '#CVmhRumungsklage/view/' + response.id,
                        { trigger: true }
                    );
                })
                .catch(xhr => {
                    let msg = this.view.translate('evictionError', 'messages', 'CMietobjekt');
                    if (xhr.status === 403) {
                        msg = this.view.translate('Access denied', 'messages');
                    }
                    Espo.Ui.error(msg);
                });
        }
    }
    
    return EvictionActionHandler;
});
```

**5. Translations** ([i18n/de_DE/CMietobjekt.json](custom/Espo/Custom/Resources/i18n/de_DE/CMietobjekt.json)):
```json
{
    "labels": {
        "Initiate Eviction": "Räumungsklage einleiten"
    },
    "messages": {
        "confirmEviction": "Möchten Sie wirklich eine Räumungsklage einleiten?",
        "evictionCreated": "Räumungsklage wurde erfolgreich erstellt",
        "evictionError": "Fehler beim Erstellen der Räumungsklage"
    }
}
```

---

## Checkliste für neue Implementierungen

### Vor dem Start
- [ ] Requirements klar definiert (Welche Entity? Welche Relationen?)
- [ ] Entity-Relationen in entityDefs geprüft
- [ ] ACL-Requirements geklärt
- [ ] Datenfluss dokumentiert

### Implementierung
- [ ] Controller erstellt mit `postAction[Name]` Methode
- [ ] Service erstellt mit Business-Logik
- [ ] ACL-Checks: `check()` für Source + `checkScope()` für Target
- [ ] Transaction: `start()` → `try/catch` → `commit()/rollback()`
- [ ] Null-Check nach `createEntity()`
- [ ] Repository-Caching bei Loops
- [ ] Button in clientDefs konfiguriert (`menu.detail.buttons`)
- [ ] JavaScript Handler erstellt
- [ ] Init-Methode: `init[ButtonName]()`
- [ ] Action-Methode: `action[ButtonName]()`
- [ ] Confirmation Dialog implementiert
- [ ] Error Handling mit HTTP-Status-Codes
- [ ] Translations: Deutsch + Englisch
- [ ] Labels + Messages vollständig

### Testing
- [ ] Validierung erfolgreich: `validate_and_rebuild.py`
- [ ] Browser Hard-Refresh durchgeführt
- [ ] Button erscheint in Detailansicht
- [ ] Confirmation Dialog funktioniert
- [ ] Entity wird korrekt erstellt
- [ ] Relationen sind korrekt verknüpft
- [ ] Navigation zur neuen Entity funktioniert
- [ ] ACL wird respektiert (Test mit eingeschränktem User)
- [ ] Error Cases getestet (keine Permission, Entity nicht gefunden)
- [ ] Logs geprüft: Keine Errors

### Dokumentation
- [ ] Code-Kommentare hinzugefügt
- [ ] PHPDoc vollständig
- [ ] README aktualisiert (falls generische Änderungen)

---

## Support & Weiterentwicklung

### Erweiterungen des Templates

**Optional implementierbar:**
- Conditional Button Visibility (nur unter bestimmten Bedingungen)
- Batch Operations (mehrere Entities gleichzeitig)
- Background Jobs (lange Operationen asynchron)
- Email-Benachrichtigungen nach Erstellung
- PDF-Generierung
- Webhook-Trigger
- Audit Logging

### Performance-Optimierungen

Bei großen Datenmengen (> 100 Relations):
- Batch-Insert mit `saveEntity()` statt `createEntity()`
- Eager Loading mit `with()`
- Query Optimization mit `select()` und `where()`
- Background Jobs für lange Operationen

### Nützliche Links

- [EspoCRM Dokumentation](https://docs.espocrm.com)
- [GitHub Repository](https://github.com/espocrm/espocrm)
- [Community Forum](https://forum.espocrm.com)
- [API Dokumentation](https://docs.espocrm.com/development/api/)

---

## Versionierung

| Version | Datum | Änderungen |
|---------|-------|------------|
| 1.0 | 24.01.2026 | Initiale Version basierend auf Räumungsklage-Implementierung |

---

**Erstellt von:** EspoCRM Development Team  
**Letzte Aktualisierung:** 24. Januar 2026  
**Status:** Production Ready ✅
