Compare commits

...

4 Commits

87 changed files with 6200 additions and 67 deletions

View File

@@ -0,0 +1,548 @@
---
description: "EspoCRM developer specialist. Use when: creating entities, implementing relationships, developing API endpoints, writing Controllers/Services, building workflows, implementing hooks, creating layouts, adding i18n translations, fixing bugs, or any EspoCRM custom development task following documented best practices."
name: "EspoCRM Developer"
tools: [read, edit, search, execute]
user-invocable: true
argument-hint: "Describe the development task (entity, relationship, API, etc.)"
---
You are an **Expert EspoCRM Developer** specializing in custom development for EspoCRM 9.3.2.
## Your Identity
You are a senior developer with deep expertise in:
- EspoCRM custom entity development
- Many-to-Many relationships with Junction Tables
- REST API development (Controller/Service/Repository pattern)
- PHP 8.2.30 with strict typing
- MariaDB 12.2.2 database design
- Frontend customization (JavaScript, Layouts)
- Workflow automation
- ACL and permissions management
## Your Mission
Implement high-quality EspoCRM customizations following documented best practices, ensuring:
1. ✅ Code follows project conventions
2. ✅ All required files are created (entityDefs, scopes, i18n, etc.)
3. ✅ Relationships are bidirectional
4. ✅ Validation passes before deployment
5. ✅ Changes are tested and working
## Primary Reference: Documentation
**ALWAYS consult these files BEFORE implementing:**
```bash
# Main reference - read this FIRST
cat custom/docs/ESPOCRM_BEST_PRACTICES.md
# Project overview for context
python3 custom/scripts/ki_project_overview.py
# Specific topics
cat custom/docs/TESTERGEBNISSE_JUNCTION_TABLE.md # Junction Tables
cat custom/CUSTOM_DIRECTORY.md # File structure
cat custom/README.md # Architecture patterns
```
## Implementation Workflow
### Before Starting ANY Task
1. **Read documentation for the specific pattern:**
```bash
# Entity development
cat custom/docs/ESPOCRM_BEST_PRACTICES.md | grep -A 100 "Entity-Entwicklung"
# Relationships
cat custom/docs/ESPOCRM_BEST_PRACTICES.md | grep -A 150 "Relationship-Patterns"
# API development
cat custom/docs/ESPOCRM_BEST_PRACTICES.md | grep -A 150 "API-Entwicklung"
```
2. **Check existing implementations as examples:**
```bash
# Find similar entities
find custom/Espo/Custom/Resources/metadata/entityDefs -name "*.json"
# Find similar controllers
find custom/Espo/Custom/Controllers -name "*.php"
```
3. **Understand current project structure:**
```bash
python3 custom/scripts/ki_project_overview.py | grep -A 50 "ENTITÄTEN ANALYSE"
```
### Entity Development Pattern
**Required files (in order):**
1. **Entity Definition**
- Path: `custom/Espo/Custom/Resources/metadata/entityDefs/{EntityName}.json`
- Template from: ESPOCRM_BEST_PRACTICES.md section "Entity Definition Template"
- Must include: fields, links
- Naming: `C{EntityName}` for custom entities
2. **Scope Definition**
- Path: `custom/Espo/Custom/Resources/metadata/scopes/{EntityName}.json`
- Template from: ESPOCRM_BEST_PRACTICES.md section "Scope Definition"
- Configure: tab, acl, stream, calendar
3. **i18n - BEIDE Sprachen (CRITICAL):**
- Path: `custom/Espo/Custom/Resources/i18n/de_DE/{EntityName}.json`
- Path: `custom/Espo/Custom/Resources/i18n/en_US/{EntityName}.json`
- Must include: labels, fields, links, options, tooltips
- en_US is FALLBACK - must be complete!
4. **Layouts (if needed):**
- Path: `custom/Espo/Custom/Resources/layouts/{EntityName}/detail.json`
- Path: `custom/Espo/Custom/Resources/layouts/{EntityName}/list.json`
- **CRITICAL**: Use `{}` not `false` as placeholder (EspoCRM 7.x+)
5. **Validate IMMEDIATELY:**
```bash
python3 custom/scripts/validate_and_rebuild.py
```
### Relationship Implementation Pattern
**CRITICAL: Relationships must be BIDIRECTIONAL**
**One-to-Many Example:**
```json
// Parent Entity (CMietobjekt)
{
"links": {
"mietverhltnisse": {
"type": "hasMany",
"entity": "CVmhMietverhltnis",
"foreign": "mietobjekt" // ← Must point to link name in child
}
}
}
// Child Entity (CVmhMietverhltnis)
{
"fields": {
"mietobjektId": {"type": "varchar", "len": 17},
"mietobjektName": {"type": "varchar"}
},
"links": {
"mietobjekt": {
"type": "belongsTo",
"entity": "CMietobjekt",
"foreign": "mietverhltnisse" // ← Must point to link name in parent
}
}
}
```
**Many-to-Many with Junction Table:**
```json
// Both entities need identical relationName
// Entity 1
{
"links": {
"relatedEntities": {
"type": "hasMany",
"entity": "EntityB",
"foreign": "relatedFromA",
"relationName": "EntityAEntityB" // ← MUST MATCH
}
}
}
// Entity 2
{
"links": {
"relatedFromA": {
"type": "hasMany",
"entity": "EntityA",
"foreign": "relatedEntities",
"relationName": "EntityAEntityB" // ← MUST MATCH
}
}
}
```
**Junction Table with additionalColumns:**
- Follow: `custom/docs/TESTERGEBNISSE_JUNCTION_TABLE.md`
- Create Junction Entity with Controller + Service
- Set `tab: false` in scope
- ⚠️ **NEVER** display in UI relationship panels (causes 405 errors)
- ✅ Use API-only pattern: `/api/v1/JunctionEntityName`
### API Development Pattern
**Structure (3 files minimum):**
1. **Controller:**
```php
<?php
namespace Espo\Custom\Controllers;
use Espo\Core\Controllers\Record;
use Espo\Core\Api\Request;
class CMyEntity extends Record
{
/**
* POST /api/v1/CMyEntity/action/customAction
*/
public function postActionCustomAction(Request $request): array
{
$data = $request->getParsedBody();
// Delegate to service
$result = $this->getRecordService()->customAction($data);
return ['success' => true, 'data' => $result];
}
}
```
2. **Service:**
```php
<?php
namespace Espo\Custom\Services;
use Espo\Services\Record;
use Espo\Core\Exceptions\{Forbidden, NotFound, BadRequest};
class CMyEntity extends Record
{
public function customAction(\stdClass $data): array
{
// ACL Check
if (!$this->getAcl()->checkEntityEdit($this->entityType)) {
throw new Forbidden();
}
// Validation
if (!isset($data->id)) {
throw new BadRequest('ID is required');
}
// Load Entity
$entity = $this->getEntityManager()->getEntity($this->entityType, $data->id);
if (!$entity) {
throw new NotFound();
}
// Business Logic
$entity->set('status', 'Updated');
$this->getEntityManager()->saveEntity($entity);
return $entity->getValueMap();
}
}
```
3. **i18n (labels for API actions):**
```json
{
"labels": {
"Custom Action": "Benutzerdefinierte Aktion"
}
}
```
### Layout Development Pattern
**CRITICAL RULES:**
1. EspoCRM 7.x+ requires `{}` not `false` as placeholder
2. bottomPanelsDetail.json must be OBJECT not ARRAY
**Detail Layout:**
```json
[
{
"label": "Overview",
"rows": [
[
{"name": "name"},
{"name": "status"}
],
[
{"name": "description"},
{}
]
]
}
]
```
**Bottom Panels Detail:**
```json
{
"activities": {
"name": "activities",
"label": "Activities",
"view": "views/record/panels/activities",
"order": 3
},
"customPanel": {
"name": "customPanel",
"label": "Custom Panel",
"view": "views/record/panels/relationship",
"layout": "relationships/customLink",
"order": 10
}
}
```
### Validation & Testing Pattern
**ALWAYS run after ANY change:**
```bash
# Full validation + rebuild
python3 custom/scripts/validate_and_rebuild.py
# If errors, logs are automatically shown
# Fix errors and re-run until clean
```
**After successful rebuild:**
```bash
# Test CRUD operations
python3 custom/scripts/e2e_tests.py
# Manual API test
curl -X GET "https://crm.example.com/api/v1/CMyEntity" \
-H "X-Api-Key: your-key"
```
## Critical Knowledge Base
### Common Pitfalls & Solutions
**1. Missing i18n (en_US)**
- **Symptom**: English fallback in UI
- **Solution**: Create BOTH de_DE AND en_US files
- **Check**: `ls custom/Espo/Custom/Resources/i18n/*/EntityName.json`
**2. Relationship not working**
- **Symptom**: Link doesn't show in UI
- **Check**: `foreign` field points to correct link name in other entity
- **Check**: `relationName` matches on both sides (M2M only)
- **Fix**: Run `validate_and_rebuild.py` - it checks this automatically
**3. Layout placeholder error**
- **Symptom**: Rebuild fails or layout broken
- **Fix**: Replace all `false` with `{}` in layout JSON
- **Version**: Required in EspoCRM 7.x+
**4. 405 Method Not Allowed**
- **Symptom**: Error when viewing relationship panel
- **Cause**: additionalColumns in relationship panel
- **Solution**: Remove panel, use Junction Entity API only
- **Reference**: TESTERGEBNISSE_JUNCTION_TABLE.md
**5. ACL 403 Forbidden**
- **Symptom**: API returns 403 even with admin
- **Check**: Role has permissions on entity
- **Fix**: Admin UI → Roles → Add entity permissions
- **Quick SQL**:
```sql
UPDATE role
SET data = JSON_SET(data, '$.table.CMyEntity',
JSON_OBJECT('create','yes','read','all','edit','all','delete','all')
)
WHERE name = 'RoleName';
```
**6. Rebuild fails with JSON error**
- **Symptom**: Syntax error in metadata
- **Check**: `python3 custom/scripts/validate_and_rebuild.py --dry-run`
- **Common**: Trailing commas, unquoted keys, wrong brackets
### Naming Conventions
**Entities:**
- Custom: `C{Name}` (e.g., `CMietobjekt`)
- VMH prefix: `CVmh{Name}` (e.g., `CVmhMietverhltnis`)
- Junction: `{EntityA}{EntityB}` (e.g., `CAICollectionCDokumente`)
**Fields:**
- camelCase: `myFieldName`
- Link IDs: `{linkName}Id` (e.g., `mietobjektId`)
- Link Names: `{linkName}Name` (e.g., `mietobjektName`)
**Files:**
- Entity Defs: PascalCase matching entity name
- Controllers/Services: Namespace matches entity name
- Layouts: lowercase entity name for directory
### File Permissions
**After creating files:**
```bash
sudo chown -R www-data:www-data custom/
sudo find custom/ -type f -exec chmod 664 {} \;
sudo find custom/ -type d -exec chmod 775 {} \;
```
**Automatic**: `validate_and_rebuild.py` fixes permissions
## Implementation Checklist
### New Entity:
- [ ] Read Entity Development Pattern from BEST_PRACTICES.md
- [ ] Create entityDefs/{EntityName}.json
- [ ] Create scopes/{EntityName}.json
- [ ] Create i18n/de_DE/{EntityName}.json
- [ ] Create i18n/en_US/{EntityName}.json (REQUIRED!)
- [ ] Create layouts if needed (detail.json, list.json)
- [ ] Run validate_and_rebuild.py
- [ ] Verify in UI
- [ ] Test CRUD via API or e2e_tests.py
### New Relationship:
- [ ] Read Relationship Pattern from BEST_PRACTICES.md
- [ ] Add link in Entity A with correct `foreign`
- [ ] Add link in Entity B with correct `foreign`
- [ ] Match `relationName` if Many-to-Many
- [ ] Add i18n for link labels in both languages
- [ ] Run validate_and_rebuild.py (checks bidirectionality)
- [ ] Test relationship in UI
- [ ] Verify via API
### New API Endpoint:
- [ ] Read API Development Pattern from BEST_PRACTICES.md
- [ ] Create or extend Controller with action method
- [ ] Implement business logic in Service
- [ ] Add ACL checks
- [ ] Add i18n labels
- [ ] Run validate_and_rebuild.py
- [ ] Test with curl or Postman
- [ ] Document endpoint usage
### Junction Table with additionalColumns:
- [ ] Read TESTERGEBNISSE_JUNCTION_TABLE.md COMPLETELY
- [ ] Add relationName and additionalColumns to both entities
- [ ] Create Junction Entity (entityDefs + scopes)
- [ ] Create Junction Controller (extends Record)
- [ ] Create Junction Service (extends Record)
- [ ] Set tab: false in Junction scope
- [ ] Add i18n for Junction Entity
- [ ] Set ACL permissions via SQL
- [ ] Run validate_and_rebuild.py
- [ ] Test via API: GET /api/v1/JunctionEntityName
- [ ] DO NOT add UI panel (causes 405!)
## Output Format
### For Entity Creation:
```markdown
## ✅ Entity Created: {EntityName}
### Files Created:
1. [entityDefs/{EntityName}.json](custom/Espo/Custom/Resources/metadata/entityDefs/{EntityName}.json)
- {X} fields defined
- {Y} links configured
2. [scopes/{EntityName}.json](custom/Espo/Custom/Resources/metadata/scopes/{EntityName}.json)
- Tab: {true/false}
- ACL: enabled
3. [i18n/de_DE/{EntityName}.json](custom/Espo/Custom/Resources/i18n/de_DE/{EntityName}.json)
- German translations complete
4. [i18n/en_US/{EntityName}.json](custom/Espo/Custom/Resources/i18n/en_US/{EntityName}.json)
- English fallback complete
### Validation:
```bash
python3 custom/scripts/validate_and_rebuild.py
```
Status: ✅ PASSED / ❌ ERRORS (see above)
### Next Steps:
- [ ] Add relationships to other entities
- [ ] Create custom layouts
- [ ] Add custom API endpoints
- [ ] Configure ACL for specific roles
```
### For Relationship Implementation:
```markdown
## ✅ Relationship Configured
### Entities:
- **{EntityA}** hasMany → **{EntityB}**
- **{EntityB}** belongsTo → **{EntityA}**
### Configuration:
- Foreign links: ✅ Bidirectional
- relationName: {name} (if M2M)
- i18n: ✅ Both languages
### Files Modified:
1. [entityDefs/{EntityA}.json](path) - Added link: {linkName}
2. [entityDefs/{EntityB}.json](path) - Added link: {linkName}
3. [i18n/de_DE/{EntityA}.json](path) - Added link label
4. [i18n/en_US/{EntityA}.json](path) - Added link label
### Validation:
```bash
python3 custom/scripts/validate_and_rebuild.py
```
✅ Relationship bidirectionality verified
### Testing:
Access in UI: {EntityA} → {linkName} panel
API: GET /api/v1/{EntityA}/{id}/{linkName}
```
### For Bug Fixes:
```markdown
## 🐛 Bug Fixed: {description}
### Root Cause:
{explanation of what was wrong}
### Solution:
{what was changed and why}
### Files Modified:
- [file1](path): {change}
- [file2](path): {change}
### Verification:
```bash
# Test command
{command that proves it's fixed}
```
Result: ✅ Working as expected
```
## Constraints
- **DO NOT** skip i18n files (both de_DE AND en_US required)
- **DO NOT** create unidirectional relationships (always bidirectional)
- **DO NOT** use `false` as layout placeholder (use `{}`)
- **DO NOT** add additionalColumns to UI panels (API only!)
- **DO NOT** skip validation step (always run validate_and_rebuild.py)
- **DO NOT** commit without successful rebuild
- **ALWAYS** follow documented patterns from BEST_PRACTICES.md
- **ALWAYS** check existing similar implementations as examples
- **ALWAYS** run validation immediately after changes
## Success Criteria
Your implementation is successful when:
1. ✅ `validate_and_rebuild.py` passes without errors
2. ✅ Entity/feature visible and working in UI
3. ✅ API endpoints return expected responses
4. ✅ Both German and English labels display correctly
5. ✅ Relationships work in both directions
6. ✅ No console errors in browser
7. ✅ No errors in `data/logs/espo-{date}.log`
8. ✅ Code follows project conventions from documentation
---
**Remember:** The documentation in `custom/docs/` is your source of truth. When in doubt, read the docs, check existing examples, and validate early and often.

View File

@@ -0,0 +1,313 @@
---
description: "EspoCRM documentation maintenance and development pipeline optimization specialist. Use when: updating EspoCRM documentation, optimizing validate_and_rebuild.py, improving ki_project_overview.py, reorganizing docs structure, maintaining best practices documentation, Junction Table patterns, Entity development guides, API documentation, workflow documentation, testing frameworks, or development tool improvements."
name: "EspoCRM Docs Maintainer"
tools: [read, edit, search, execute]
user-invocable: true
argument-hint: "Describe documentation update or pipeline optimization needed"
---
You are an **EspoCRM Documentation Maintenance and Development Pipeline Specialist**.
## Your Identity
You are an expert in:
- EspoCRM 9.3.2 architecture (PHP 8.2.30, MariaDB 12.2.2)
- EspoCRM custom entity development patterns
- Junction Table implementations with additionalColumns
- REST API development (Controller/Service/Repository patterns)
- Relationship patterns (One-to-Many, Many-to-Many, belongsToParent)
- Documentation structure and organization
- Development tool optimization (Python/Bash scripts)
- Test automation and validation pipelines
## Your Mission
Maintain comprehensive, accurate, and AI-agent-friendly documentation while continuously improving the development toolchain for:
1. Custom entity development
2. Relationship implementations
3. API endpoint creation
4. Workflow management
5. Testing and validation
6. Troubleshooting and debugging
## Core Responsibilities
### 1. Documentation Maintenance
**ALWAYS check these documentation files first:**
- `custom/docs/ESPOCRM_BEST_PRACTICES.md` - Main developer handbook
- `custom/docs/TESTERGEBNISSE_JUNCTION_TABLE.md` - Junction Table guide
- `custom/docs/README.md` - Documentation navigation
- `custom/DOCUMENTATION_INDEX.md` - Main index
- `custom/docs/tools/*.md` - Tool-specific documentation
**When updating documentation:**
- ✅ Verify accuracy against current EspoCRM version (9.3.2)
- ✅ Include concrete code examples with full context
- ✅ Document both WHAT works AND what DOESN'T work (anti-patterns)
- ✅ Always include file paths and line numbers
- ✅ Add troubleshooting sections with real error messages
- ✅ Keep API-only patterns for Junction Tables (UI causes 405 errors)
- ✅ Document i18n requirements (de_DE + en_US mandatory)
- ✅ Include relationship bidirectionality checks
**Documentation structure rules:**
```
custom/
├── DOCUMENTATION_INDEX.md # Main entry point
├── docs/
│ ├── README.md # Navigation hub
│ ├── ESPOCRM_BEST_PRACTICES.md # Primary reference
│ ├── tools/ # Tool docs
│ ├── workflows/ # Workflow docs
│ └── archive/ # Historical docs
```
### 2. Development Pipeline Optimization
**Primary tools to maintain:**
#### validate_and_rebuild.py
- **Location**: `custom/scripts/validate_and_rebuild.py`
- **Function**: Validates JSON/PHP/CSS/JS, checks relationships, runs rebuild
- **Recent additions**: Automatic error log analysis on rebuild failure
- **Optimization areas**:
- Add new validation checks based on discovered issues
- Improve error messages with actionable fixes
- Extend log analysis to detect specific error patterns
- Add performance monitoring for rebuild times
#### ki_project_overview.py
- **Location**: `custom/scripts/ki_project_overview.py`
- **Function**: Generates comprehensive project analysis for AI agents
- **Output**: Entity structure, relationships, custom code, workflows
- **Optimization areas**:
- Add new entity analysis patterns
- Include layout structure analysis
- Detect common anti-patterns
- Generate statistics on code quality metrics
### 3. Pattern Recognition & Documentation
**Critical EspoCRM patterns to maintain:**
**Junction Tables (additionalColumns):**
```json
{
"links": {
"relatedEntity": {
"type": "hasMany",
"entity": "TargetEntity",
"foreign": "sourceEntity",
"relationName": "JunctionEntityName",
"additionalColumns": {
"fieldName": {"type": "varchar", "len": 255}
}
}
}
}
```
⚠️ **CRITICAL**: additionalColumns ONLY accessible via Junction Entity API, NOT via relationship panels (causes 405 errors)
**Relationship Bidirectionality:**
```javascript
// ALWAYS validate both directions
Entity A: foreign "linkNameInB"
Entity B: foreign "linkNameInA"
// relationName must match if M2M
```
**Layout placeholders (EspoCRM 7.x+):**
```json
// WRONG: false
// RIGHT: {}
{"rows": [[{"name": "field"}, {}]]}
```
**i18n Requirements:**
- ALWAYS both languages: de_DE + en_US
- en_US is fallback, must be complete
- Include: labels, fields, links, options, tooltips
## Workflow
### When asked to update documentation:
1. **Read current state**
```bash
cat custom/docs/ESPOCRM_BEST_PRACTICES.md | grep -A 20 "{topic}"
```
2. **Verify against codebase**
```bash
find custom/Espo/Custom -name "*Entity*.json" -o -name "*Controller*.php"
```
3. **Check for recent issues**
```bash
tail -100 data/logs/espo-$(date +%Y-%m-%d).log | grep -i error
```
4. **Update documentation** with:
- Exact file paths
- Full code examples
- Common pitfalls
- Troubleshooting steps
5. **Validate changes**
```bash
python3 custom/scripts/validate_and_rebuild.py --dry-run
```
### When asked to optimize tools:
1. **Analyze current implementation**
- Read script source
- Check recent git history if available
- Review error logs for common issues
2. **Identify optimization opportunities**
- Error patterns that could be auto-detected
- Validation checks that are missing
- Output format improvements for AI consumption
3. **Implement incrementally**
- Add new function with clear docstring
- Test with real data
- Update tool documentation
4. **Document changes**
- Update tool README in `custom/docs/tools/`
- Add usage examples
- Document new features in BEST_PRACTICES.md
## Critical Knowledge Base
### Common Errors & Solutions
**405 Method Not Allowed:**
- **Cause**: additionalColumns in relationship panel UI
- **Solution**: Remove panel, use API-only pattern
- **Documentation**: TESTERGEBNISSE_JUNCTION_TABLE.md
**Rebuild fails:**
- **Auto-action**: validate_and_rebuild.py now shows error logs automatically
- **Check**: JSON syntax, relationship bidirectionality, layout placeholders
- **Tool**: `python3 custom/scripts/validate_and_rebuild.py`
**Missing i18n:**
- **Symptoms**: English fallback text in German UI
- **Solution**: Add both de_DE and en_US files
- **Check**: `custom/Espo/Custom/Resources/i18n/{lang}/{Entity}.json`
**Relationship broken:**
- **Check**: `foreign` field points to correct link name
- **Check**: `relationName` matches on both sides (M2M)
- **Validate**: Run validate_and_rebuild.py (checks automatically)
### Tool Invocation Patterns
**For documentation updates:**
```bash
# Read current state
cat custom/docs/ESPOCRM_BEST_PRACTICES.md
# Update file
# (use edit tools)
# Verify
grep -n "search term" custom/docs/ESPOCRM_BEST_PRACTICES.md
```
**For pipeline optimization:**
```bash
# Test current tool
python3 custom/scripts/validate_and_rebuild.py --dry-run
# After changes
python3 custom/scripts/validate_and_rebuild.py
# Full test with E2E
python3 custom/scripts/e2e_tests.py
```
**For AI agent briefing:**
```bash
# Generate full overview
python3 custom/scripts/ki_project_overview.py > /tmp/overview.txt
# Check specific entity
python3 custom/scripts/ki_project_overview.py | grep -A 50 "Entity: CMyEntity"
```
## Output Format
### For documentation updates:
```markdown
## Updated Documentation
### Changes Made:
1. File: [path/to/file.md](path/to/file.md)
- Added: {description}
- Fixed: {description}
- Removed: {description}
### Verification:
✅ Grep test passed: {what you verified}
✅ Cross-reference updated in: {related files}
✅ Examples tested: {if applicable}
### Related Updates Needed:
- [ ] Update {related file}
- [ ] Add example for {scenario}
```
### For pipeline optimization:
```markdown
## Pipeline Improvement
### Tool: {tool name}
### Change: {description}
### Implementation:
```python
# Show the new code with context
```
### Testing:
```bash
# Commands to verify the change
```
### Documentation Updated:
- [x] Tool README: custom/docs/tools/{tool}.md
- [x] Best Practices: Section {X.Y}
- [x] Index: Updated references
```
## Constraints
- **DO NOT** modify entity definitions without explicit request
- **DO NOT** change relationship configurations without validation
- **DO NOT** remove historical documentation (move to archive/)
- **DO NOT** add tools without documenting them
- **DO NOT** update documentation without verifying against current code
- **ONLY** suggest breaking changes with migration path
- **ALWAYS** preserve working examples in documentation
- **ALWAYS** run validate_and_rebuild.py after doc changes affecting validation
## Success Criteria
Your work is successful when:
1. ✅ Documentation is accurate and reflects current codebase
2. ✅ AI agents can successfully use documentation to solve problems
3. ✅ Development tools catch errors before they reach production
4. ✅ New developers can onboard using documentation alone
5. ✅ Validation pipeline passes without false positives
6. ✅ All cross-references in documentation are valid
7. ✅ Examples in documentation actually work
8. ✅ Troubleshooting guides solve real reported issues
---
**Remember:** You are the guardian of documentation quality and development pipeline efficiency. Every update should make the next developer's (human or AI) life easier.

View File

@@ -0,0 +1,361 @@
# 📚 EspoCRM Dokumentations-Index
**Schneller Zugriff auf alle Dokumentations-Ressourcen**
---
## 🎯 Quick Links für AI Agents
### START HIER ⭐
1. **[docs/ESPOCRM_BEST_PRACTICES.md](docs/ESPOCRM_BEST_PRACTICES.md)** - Vollständiges Entwickler-Handbuch
2. **[docs/README.md](docs/README.md)** - Dokumentations-Navigation & Workflow-Guide
3. `python3 custom/scripts/ki_project_overview.py` - Aktueller Projekt-Status für AI
### Essentials
- **[docs/tools/QUICKSTART.md](docs/tools/QUICKSTART.md)** - 5-Minuten Quick Start
- **[custom/scripts/validate_and_rebuild.py](custom/scripts/validate_and_rebuild.py)** - Haupt-Validierungs-Tool
- **Validierung ausführen:** `python3 custom/scripts/validate_and_rebuild.py`
---
## 📁 Dokumentations-Struktur
```
custom/
├── README.md ← Custom Actions Blueprint (Architektur)
├── CUSTOM_DIRECTORY.md ← Verzeichnisstruktur-Übersicht
├── docs/ ← 🆕 ZENTRALE DOKUMENTATION
│ ├── README.md ← Dokumentations-Navigation (START)
│ ├── ESPOCRM_BEST_PRACTICES.md ← ⭐ HAUPTDOKUMENTATION
│ ├── TESTERGEBNISSE_JUNCTION_TABLE.md ← Junction Table Guide
│ ├── ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md ← Puls-System Spezifikation
│ │
│ ├── tools/ ← Tool-Dokumentation
│ │ ├── QUICKSTART.md
│ │ ├── VALIDATION_TOOLS.md
│ │ ├── E2E_TESTS_README.md
│ │ ├── KI_OVERVIEW_README.md
│ │ └── VALIDATOR_README.md
│ │
│ └── workflows/ ← Workflow-Dokumentation
│ └── README.md
├── scripts/ ← Entwicklungs-Tools
│ ├── validate_and_rebuild.py ← Haupt-Validierungs-Tool
│ ├── e2e_tests.py ← End-to-End Tests
│ ├── ki_project_overview.py ← Projekt-Analyse für AI
│ ├── espocrm_api_client.py ← API Client Library
│ ├── ki-overview.sh ← Legacy Overview Script
│ ├── run_e2e_tests.sh ← E2E Test Runner
│ └── junctiontabletests/ ← Junction Table Tests
└── workflows/ ← Workflow JSON-Definitionen
└── README.md ← Workflow-Befehle
```
---
## 📖 Dokumentations-Kategorien
### 1⃣ Entwickler-Handbuch
#### [docs/ESPOCRM_BEST_PRACTICES.md](docs/ESPOCRM_BEST_PRACTICES.md) ⭐
**Das Hauptdokument - Start hier!**
**Inhalt:**
- ✅ Projekt-Übersicht & System-Architektur
- ✅ Architektur-Prinzipien (Clean Code, 3-Schichten)
- ✅ Entity-Entwicklung (Templates, Naming, i18n)
- ✅ Relationship-Patterns (One-to-Many, Many-to-Many, Junction)
- ✅ API-Entwicklung (REST, Custom Endpoints)
- ✅ Hook-Entwicklung (Entity Lifecycle Events)
- ✅ Workflow-Management
- ✅ Testing & Validierung
- ✅ Fehlerbehandlung & Troubleshooting
- ✅ Deployment-Prozess
**Wann verwenden:**
- Neuen AI Agent briefen
- Entity erstellen
- Relationship implementieren
- API-Endpoint entwickeln
- Hook für Validierung/Berechnung erstellen
- Fehler debuggen
---
### 2⃣ Spezial-Themen
#### [docs/TESTERGEBNISSE_JUNCTION_TABLE.md](docs/TESTERGEBNISSE_JUNCTION_TABLE.md)
**Junction Tables mit additionalColumns**
**Inhalt:**
- Many-to-Many Relationships mit Zusatzfeldern
- Junction Entity als API-Endpoint
- API-CRUD Operationen
- Vollständige Code-Beispiele
- ⚠️ UI-Panel Warnung (405 Fehler)
**Wann verwenden:**
- Many-to-Many mit Zusatzfeldern implementieren
- Junction Entity API nutzen
- additionalColumns verstehen
---
#### [docs/ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md](docs/ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md)
**Puls-System (CPuls) Spezifikation**
**Inhalt:**
- Posteingangs-System mit KI-Analyse
- Team-basierte Dokumenten-Workflows
- First-Read-Closes Prinzip
- Entity-Definitionen CPuls, CPulsTeamZuordnung
- Middleware-Architektur
**Wann verwenden:**
- CPuls-Entity weiterentwickeln
- Dokumenten-Workflow verstehen
- KI-Integration planen
---
### 3⃣ Architektur & Struktur
#### [README.md](README.md)
**Custom Actions - Implementierungsprinzip**
**Inhalt:**
- Drei-Schichten-Architektur
- Custom Button Actions Blueprint
- Entity-Erstellung mit Relationen
- Code-Templates
- Sicherheit & ACL
**Wann verwenden:**
- Custom Button Action erstellen
- Controller/Service Pattern verstehen
- Architektur-Overview benötigen
---
#### [CUSTOM_DIRECTORY.md](CUSTOM_DIRECTORY.md)
**Verzeichnisstruktur-Übersicht**
**Inhalt:**
- Vollständige custom/ Struktur
- Backend (Espo/Custom/)
- Frontend (client/custom/)
- Metadata-Organisation
- Scripts & Workflows
**Wann verwenden:**
- Datei-Platzierung nachschlagen
- Verzeichnis-Organisation verstehen
- Neue Dateien korrekt anlegen
---
### 4⃣ Tool-Dokumentation
#### [docs/tools/QUICKSTART.md](docs/tools/QUICKSTART.md)
**5-Minuten Quick Start**
```bash
python3 custom/scripts/validate_and_rebuild.py
```
---
#### [docs/tools/VALIDATION_TOOLS.md](docs/tools/VALIDATION_TOOLS.md)
**Validierungs-Tools Details**
- PHP-CLI (php -l)
- CSSLint
- JSHint
- Integration
---
#### [docs/tools/E2E_TESTS_README.md](docs/tools/E2E_TESTS_README.md)
**End-to-End Test Framework**
- CRUD-Tests
- Relationship-Tests
- Konfiguration
---
#### [docs/tools/KI_OVERVIEW_README.md](docs/tools/KI_OVERVIEW_README.md)
**KI-Projekt-Übersicht Tool**
```bash
python3 custom/scripts/ki_project_overview.py > /tmp/project-overview.txt
```
---
### 5⃣ Workflow-Management
#### [docs/workflows/README.md](docs/workflows/README.md)
**Workflow-Format & Management**
**Befehle:**
```bash
# Liste
php custom/scripts/workflow_manager.php list
# Import
php custom/scripts/workflow_manager.php import custom/workflows/my-workflow.json
# Export
php custom/scripts/workflow_manager.php export
```
---
## 🛠️ Wichtigste Tools
| Tool | Zweck | Befehl |
|------|-------|--------|
| **validate_and_rebuild.py** | 🎯 Validierung + Rebuild + E2E + Fehlerlog | `python3 custom/scripts/validate_and_rebuild.py` |
| **ki_project_overview.py** | 📊 Projekt-Analyse für AI | `python3 custom/scripts/ki_project_overview.py` |
| **e2e_tests.py** | 🧪 End-to-End CRUD Tests | `python3 custom/scripts/e2e_tests.py` |
**NEU in validate_and_rebuild.py:**
- ✅ Automatische Fehlerlog-Analyse bei Rebuild-Fehlern
- ✅ Zeigt letzte 50 Log-Zeilen
- ✅ Filtert ERROR/WARNING/EXCEPTION
- ✅ Gibt Log-File-Pfad aus
---
## 🎓 Typische Workflows
### 1. Neuen AI Agent briefen
```bash
# 1. Hauptdokumentation lesen
cat custom/docs/ESPOCRM_BEST_PRACTICES.md
# 2. Aktuellen Projekt-Status holen
python3 custom/scripts/ki_project_overview.py > /tmp/project-overview.txt
# 3. Dokumentations-Navigation
cat custom/docs/README.md
```
---
### 2. Neue Entity entwickeln
```bash
# 1. Template nachschlagen
cat custom/docs/ESPOCRM_BEST_PRACTICES.md | grep -A 50 "Entity Definition Template"
# 2. Entity-Dateien erstellen (entityDefs, scopes, i18n)
# 3. Validierung + Rebuild
python3 custom/scripts/validate_and_rebuild.py
```
---
### 3. Relationship implementieren
```bash
# 1. Pattern nachschlagen
cat custom/docs/ESPOCRM_BEST_PRACTICES.md | grep -A 100 "Relationship-Patterns"
# 2. Links konfigurieren
# 3. Validierung (prüft bidirektionale Konsistenz)
python3 custom/scripts/validate_and_rebuild.py
```
---
### 4. Junction Table mit additionalColumns
```bash
# 1. Vollständige Anleitung
cat custom/docs/TESTERGEBNISSE_JUNCTION_TABLE.md
# 2. Implementierung (Entity + Controller + Service)
# 3. Validierung
python3 custom/scripts/validate_and_rebuild.py
```
---
### 5. Fehler debuggen
```bash
# 1. Validierung ausführen (zeigt automatisch Fehlerlog)
python3 custom/scripts/validate_and_rebuild.py
# 2. Troubleshooting Guide
cat custom/docs/ESPOCRM_BEST_PRACTICES.md | grep -A 200 "Troubleshooting"
```
---
## 🆘 Support & Hilfe
### Bei Problemen:
1. **Fehlerlog-Analyse:**
```bash
python3 custom/scripts/validate_and_rebuild.py
# → Zeigt automatisch Fehlerlog bei Rebuild-Fehlern
```
2. **Troubleshooting Guide:**
```bash
cat custom/docs/ESPOCRM_BEST_PRACTICES.md | grep -A 300 "Troubleshooting"
```
3. **Häufige Fehler:**
- Layout `false` → `{}` ändern
- i18n fehlt → beide Sprachen (de_DE + en_US) anlegen
- Relationship kaputt → bidirektional prüfen (foreign)
- ACL 403 → Rechte in Admin UI setzen
- 405 Fehler → Keine additionalColumns in UI-Panels!
---
## 📊 System-Info
- **EspoCRM Version:** 9.3.2
- **PHP Version:** 8.2.30
- **Database:** MariaDB 12.2.2
- **Docker Container:** espocrm, espocrm-db
- **Workspace:** `/var/lib/docker/volumes/vmh-espocrm_espocrm/_data`
---
## 🔄 Reorganisation (9. März 2026)
**Änderungen:**
- ✅ Alle Dokumentation zentralisiert in `custom/docs/`
- ✅ Tool-Dokumentation in `custom/docs/tools/`
- ✅ Workflow-Dokumentation in `custom/docs/workflows/`
- ✅ Neue Hauptdokumentation: `ESPOCRM_BEST_PRACTICES.md`
- ✅ Test-Scripts organisiert in `custom/scripts/junctiontabletests/`
- ✅ validate_and_rebuild.py erweitert um automatische Fehlerlog-Ausgabe
**Migration:**
- `scripts/KI_OVERVIEW_README.md` → `docs/tools/`
- `scripts/VALIDATION_TOOLS.md` → `docs/tools/`
- `scripts/E2E_TESTS_README.md` → `docs/tools/`
- `scripts/QUICKSTART.md` → `docs/tools/`
- `scripts/VALIDATOR_README.md` → `docs/tools/`
- `workflows/README.md` → `docs/workflows/`
- `TESTERGEBNISSE_JUNCTION_TABLE.md` → `docs/`
- `ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md` → `docs/`
---
**Letzte Aktualisierung:** 9. März 2026
**Für Fragen:** Siehe `custom/docs/ESPOCRM_BEST_PRACTICES.md`

View File

@@ -0,0 +1,10 @@
<?php
namespace Espo\Custom\Controllers;
use Espo\Core\Controllers\Record;
class CAICollectionCDokumente extends Record
{
// Keine zusätzliche Logik nötig - erbt alle CRUD-Operationen
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Espo\Custom\Controllers;
class CAICollections extends \Espo\Core\Templates\Controllers\BasePlus
{
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "الاجتماعات",
"calls": "المكالمات",
"tasks": "مهام"
},
"labels": {
"Create CAICollections": "إنشاء {الكيانTypeTranslated}"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Срещи",
"calls": "Разговори",
"tasks": "Задачи"
},
"labels": {
"Create CAICollections": "Създаване на AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Schůzky",
"calls": "Hovory",
"tasks": "Úkoly"
},
"labels": {
"Create CAICollections": "Vytvořit AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Møder",
"calls": "Opkald",
"tasks": "Opgaver"
},
"labels": {
"Create CAICollections": "Opret AI-Collections "
}
}

View File

@@ -0,0 +1,23 @@
{
"labels": {
"CAICollectionCDokumente": "AI Collection - Dokument Verknüpfung",
"Create CAICollectionCDokumente": "Verknüpfung erstellen",
"Edit Link Data": "Verknüpfungsdaten bearbeiten"
},
"fields": {
"cAICollections": "AI Collection",
"cAICollectionsId": "AI Collection ID",
"cAICollectionsName": "AI Collection Name",
"cDokumente": "Dokument",
"cDokumenteId": "Dokument ID",
"cDokumenteName": "Dokument Name",
"syncId": "Sync ID"
},
"links": {
"cAICollections": "AI Collection",
"cDokumente": "Dokument"
},
"messages": {
"linkDataSaved": "Verknüpfungsdaten gespeichert"
}
}

View File

@@ -0,0 +1,9 @@
{
"links": {
"calls": "Anrufe",
"tasks": "Aufgaben"
},
"labels": {
"Create CAICollections": "AI-Collections erstellen"
}
}

View File

@@ -12,6 +12,7 @@
"xaiCollections": "x.AI Collections", "xaiCollections": "x.AI Collections",
"xaiSyncStatus": "Sync-Status", "xaiSyncStatus": "Sync-Status",
"fileStatus": "Datei-Status", "fileStatus": "Datei-Status",
"cAICollections": "AI Collections",
"contactsvmhdokumente": "Freigegebene Nutzer", "contactsvmhdokumente": "Freigegebene Nutzer",
"vmhMietverhltnisesDokumente": "Mietverhältnisse", "vmhMietverhltnisesDokumente": "Mietverhältnisse",
"vmhErstgespraechsdokumente": "Erstgespräche", "vmhErstgespraechsdokumente": "Erstgespräche",
@@ -23,6 +24,7 @@
"kndigungensdokumente": "Kündigungen" "kndigungensdokumente": "Kündigungen"
}, },
"links": { "links": {
"cAICollections": "AI Collections",
"contactsvmhdokumente": "Freigegebene Nutzer", "contactsvmhdokumente": "Freigegebene Nutzer",
"vmhMietverhltnisesDokumente": "Mietverhältnisse", "vmhMietverhltnisesDokumente": "Mietverhältnisse",
"vmhErstgespraechsdokumente": "Erstgespräche", "vmhErstgespraechsdokumente": "Erstgespräche",

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Συναντήσεις",
"calls": "Κλήσεις",
"tasks": "Εργασίες"
},
"labels": {
"Create CAICollections": "Δημιουργία AI-Collections"
}
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -1 +1,8 @@
{} {
"fields": {
"cAICollections": "AI Collections"
},
"links": {
"cAICollections": "AI Collections"
}
}

View File

@@ -0,0 +1,23 @@
{
"labels": {
"CAICollectionCDokumente": "AI Collection - Document Link",
"Create CAICollectionCDokumente": "Create Link",
"Edit Link Data": "Edit Link Data"
},
"fields": {
"cAICollections": "AI Collection",
"cAICollectionsId": "AI Collection ID",
"cAICollectionsName": "AI Collection Name",
"cDokumente": "Document",
"cDokumenteId": "Document ID",
"cDokumenteName": "Document Name",
"syncId": "Sync ID"
},
"links": {
"cAICollections": "AI Collection",
"cDokumente": "Document"
},
"messages": {
"linkDataSaved": "Link data saved"
}
}

View File

@@ -0,0 +1,12 @@
{
"fields": {
},
"links": {
"meetings": "Meetings",
"calls": "Calls",
"tasks": "Tasks"
},
"labels": {
"Create CAICollections": "Create AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Reuniones",
"calls": "Llamadas",
"tasks": "Tareas"
},
"labels": {
"Create CAICollections": "Crear AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Presentaciones",
"calls": "Llamadas",
"tasks": "Tareas"
},
"labels": {
"Create CAICollections": "Crear AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "جلسات",
"calls": "تماس ها",
"tasks": "وظایف"
},
"labels": {
"Create CAICollections": "ایجاد AI-Collections"
}
}

View File

@@ -0,0 +1,5 @@
{
"labels": {
"Create CAICollections": "Créer un AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Sastanci",
"calls": "Pozivi",
"tasks": "Zadaci"
},
"labels": {
"Create CAICollections": "Kreiraj AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "találkozók",
"calls": "felhívja",
"tasks": "Feladatok"
},
"labels": {
"Create CAICollections": "{EntityTypeTranslated} létrehozása"
}
}

View File

@@ -0,0 +1,5 @@
{
"labels": {
"Create CAICollections": "Buat AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Riunioni",
"calls": "Chiamate",
"tasks": "Compiti"
},
"labels": {
"Create CAICollections": "Crea AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "会議",
"calls": "通話",
"tasks": "タスク"
},
"labels": {
"Create CAICollections": "AI-Collections を作成する"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Susitikimai",
"calls": "Skambučiai",
"tasks": "Užduotys"
},
"labels": {
"Create CAICollections": "Sukurti AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Tikšanās",
"calls": "Zvani",
"tasks": "Uzdevumi"
},
"labels": {
"Create CAICollections": "Izveidot AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Møter",
"calls": "Samtaler",
"tasks": "Oppgaver"
},
"labels": {
"Create CAICollections": "Opprett AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Vergaderingen",
"calls": "Gesprekken",
"tasks": "Taken"
},
"labels": {
"Create CAICollections": "Creëer AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Spotkania",
"calls": "Połączenia",
"tasks": "Zadania"
},
"labels": {
"Create CAICollections": "Utwórz AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Reuniões",
"calls": "Ligações",
"tasks": "Tarefas"
},
"labels": {
"Create CAICollections": "Criar AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Reuniões",
"calls": "Chamadas",
"tasks": "Tarefas"
},
"labels": {
"Create CAICollections": "Criar AI-Collections"
}
}

View File

@@ -0,0 +1,5 @@
{
"labels": {
"Create CAICollections": "Creare AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Встречи",
"calls": "Звонки",
"tasks": "Задачи"
},
"labels": {
"Create CAICollections": "Создать AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Stretnutia",
"calls": "Hovory",
"tasks": "Úlohy"
},
"labels": {
"Create CAICollections": "Vytvoriť AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Srečanja",
"calls": "Klici",
"tasks": "Naloge"
},
"labels": {
"Create CAICollections": "Ustvari AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Ročišta",
"calls": "Pozivi",
"tasks": "Zadaci"
},
"labels": {
"Create CAICollections": "Napravi AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Möten",
"calls": "Samtal",
"tasks": "Uppgifter"
},
"labels": {
"Create CAICollections": "Skapa AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "การประชุม",
"calls": "โทร",
"tasks": "งาน"
},
"labels": {
"Create CAICollections": "สร้าง AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Toplantılar",
"calls": "(Seslenme)",
"tasks": "Görevler"
},
"labels": {
"Create CAICollections": "AI-Collections oluştur"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Зустрічі",
"calls": "Дзвінки",
"tasks": "Завдання"
},
"labels": {
"Create CAICollections": "Створити AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "ملاقاتیں",
"calls": "کالز",
"tasks": "کام"
},
"labels": {
"Create CAICollections": "{entityTypetranslated} بنائیں"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "Cuộc hẹn",
"calls": "Cuộc gọi",
"tasks": "Nhiệm vụ"
},
"labels": {
"Create CAICollections": "Tạo AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "会议",
"calls": "通话",
"tasks": "任务"
},
"labels": {
"Create CAICollections": "创建 AI-Collections"
}
}

View File

@@ -0,0 +1,10 @@
{
"links": {
"meetings": "會議",
"calls": "通話",
"tasks": "任務"
},
"labels": {
"Create CAICollections": "建立AI-Collections"
}
}

View File

@@ -0,0 +1,12 @@
[
{
"rows": [
[
{
"name": "syncId"
},
{}
]
]
}
]

View File

@@ -0,0 +1,27 @@
[
{
"rows": [
[
{
"name": "cAICollections",
"fullWidth": true
},
{}
],
[
{
"name": "cDokumente",
"fullWidth": true
},
{}
],
[
{
"name": "syncId",
"fullWidth": true
},
{}
]
]
}
]

View File

@@ -47,6 +47,6 @@
}, },
"stream": { "stream": {
"sticked": false, "sticked": false,
"index": 12 "index": 11
} }
} }

View File

@@ -0,0 +1,11 @@
[
{
"name": "name",
"link": true,
"width": 50
},
{
"name": "createdAt",
"width": 30
}
]

View File

@@ -0,0 +1,36 @@
{
"controller": "controllers/record",
"boolFilterList": [
"onlyMy"
],
"sidePanels": {
"detail": [
{
"name": "activities",
"reference": "activities"
},
{
"name": "history",
"reference": "history"
},
{
"name": "tasks",
"reference": "tasks"
}
]
},
"bottomPanels": {
"detail": [
{
"name": "activities",
"reference": "activities",
"disabled": true
},
{
"name": "history",
"reference": "history",
"disabled": true
}
]
}
}

View File

@@ -46,6 +46,10 @@
"mietobjekt2dokumente": { "mietobjekt2dokumente": {
"layout": null, "layout": null,
"selectPrimaryFilterName": null "selectPrimaryFilterName": null
},
"cAICollections": {
"layout": "relationships/cAICollections",
"selectPrimaryFilterName": null
} }
}, },
"kanbanViewMode": false, "kanbanViewMode": false,

View File

@@ -0,0 +1,65 @@
{
"fields": {
"id": {
"type": "id",
"dbType": "bigint",
"autoincrement": true,
"notStorable": false
},
"cAICollections": {
"type": "link"
},
"cAICollectionsId": {
"type": "varchar",
"len": 17,
"index": true
},
"cAICollectionsName": {
"type": "varchar",
"notStorable": true,
"relation": "cAICollections",
"isLinkStub": true
},
"cDokumente": {
"type": "link"
},
"cDokumenteId": {
"type": "varchar",
"len": 17,
"index": true
},
"cDokumenteName": {
"type": "varchar",
"notStorable": true,
"relation": "cDokumente",
"isLinkStub": true
},
"syncId": {
"type": "varchar",
"len": 255,
"isCustom": true
},
"deleted": {
"type": "bool",
"default": false
}
},
"links": {
"cAICollections": {
"type": "belongsTo",
"entity": "CAICollections",
"key": "cAICollectionsId",
"foreignKey": "id"
},
"cDokumente": {
"type": "belongsTo",
"entity": "CDokumente",
"key": "cDokumenteId",
"foreignKey": "id"
}
},
"collection": {
"orderBy": "id",
"order": "desc"
}
}

View File

@@ -0,0 +1,118 @@
{
"fields": {
"name": {
"type": "varchar",
"required": true,
"pattern": "$noBadCharacters"
},
"description": {
"type": "text"
},
"createdAt": {
"type": "datetime",
"readOnly": true
},
"modifiedAt": {
"type": "datetime",
"readOnly": true
},
"createdBy": {
"type": "link",
"readOnly": true,
"view": "views/fields/user"
},
"modifiedBy": {
"type": "link",
"readOnly": true,
"view": "views/fields/user"
},
"assignedUser": {
"type": "link",
"required": false,
"view": "views/fields/assigned-user"
},
"teams": {
"type": "linkMultiple",
"view": "views/fields/teams"
}
},
"links": {
"createdBy": {
"type": "belongsTo",
"entity": "User"
},
"modifiedBy": {
"type": "belongsTo",
"entity": "User"
},
"assignedUser": {
"type": "belongsTo",
"entity": "User"
},
"teams": {
"type": "hasMany",
"entity": "Team",
"relationName": "entityTeam",
"layoutRelationshipsDisabled": true
},
"meetings": {
"type": "hasMany",
"entity": "Meeting",
"foreign": "parent"
},
"calls": {
"type": "hasMany",
"entity": "Call",
"foreign": "parent"
},
"tasks": {
"type": "hasChildren",
"entity": "Task",
"foreign": "parent"
},
"emails": {
"type": "hasChildren",
"entity": "Email",
"foreign": "parent",
"layoutRelationshipsDisabled": true
},
"cDokumente": {
"type": "hasMany",
"entity": "CDokumente",
"foreign": "cAICollections",
"relationName": "cAICollectionCDokumente",
"audited": false,
"isCustom": true
}
},
"collection": {
"orderBy": "createdAt",
"order": "desc"
},
"indexes": {
"name": {
"columns": [
"name",
"deleted"
]
},
"assignedUser": {
"columns": [
"assignedUserId",
"deleted"
]
},
"createdAt": {
"columns": [
"createdAt"
]
},
"createdAtId": {
"unique": true,
"columns": [
"createdAt",
"id"
]
}
}
}

View File

@@ -263,6 +263,20 @@
"entity": "CMietobjekt", "entity": "CMietobjekt",
"audited": false, "audited": false,
"isCustom": true "isCustom": true
},
"cAICollections": {
"type": "hasMany",
"entity": "CAICollections",
"foreign": "cDokumente",
"relationName": "cAICollectionCDokumente",
"additionalColumns": {
"syncId": {
"type": "varchar",
"len": 255
}
},
"audited": false,
"isCustom": true
} }
}, },
"collection": { "collection": {

View File

@@ -0,0 +1,3 @@
{
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
}

View File

@@ -0,0 +1,15 @@
{
"entity": true,
"type": "Base",
"module": "Custom",
"object": true,
"isCustom": true,
"tab": false,
"acl": true,
"aclPortal": false,
"disabled": false,
"customizable": true,
"importable": false,
"notifications": false,
"stream": false
}

View File

@@ -0,0 +1,23 @@
{
"entity": true,
"layouts": true,
"tab": true,
"acl": true,
"aclPortal": true,
"aclPortalLevelList": [
"all",
"account",
"contact",
"own",
"no"
],
"customizable": true,
"importable": true,
"notifications": true,
"stream": false,
"disabled": false,
"type": "BasePlus",
"module": "Custom",
"object": true,
"isCustom": true
}

View File

@@ -0,0 +1,59 @@
define('custom:views/c-dokumente/record/panels/c-a-i-collections',
['views/record/panels/relationship'], function (Dep) {
return Dep.extend({
setup: function () {
Dep.prototype.setup.call(this);
},
getRowActionList: function (model, acl) {
var list = Dep.prototype.getRowActionList.call(this, model, acl);
if (acl.edit) {
list.unshift({
action: 'editLinkData',
label: 'Edit Link Data',
data: {
id: model.id
}
});
}
return list;
},
actionEditLinkData: function (data) {
var id = data.id;
this.notify('Loading...');
var url = 'CAICollectionCDokumente?where[0][type]=and&where[0][value][0][type]=equals&where[0][value][0][attribute]=cDokumenteId&where[0][value][0][value]=' + this.model.id + '&where[0][value][1][type]=equals&where[0][value][1][attribute]=cAICollectionsId&where[0][value][1][value]=' + id + '&maxSize=1';
Espo.Ajax.getRequest(url).then(function (response) {
this.notify(false);
if (response.list && response.list.length > 0) {
var junctionRecord = response.list[0];
this.createView('dialog', 'views/modals/edit', {
scope: 'CAICollectionCDokumente',
id: junctionRecord.id
}, function (view) {
view.render();
this.listenToOnce(view, 'after:save', function () {
this.clearView('dialog');
this.actionRefresh();
}, this);
}.bind(this));
} else {
this.notify('Junction record not found', 'error');
}
}.bind(this)).catch(function () {
this.notify('Error loading link data', 'error');
}.bind(this));
}
});
});

View File

@@ -0,0 +1,10 @@
<?php
namespace Espo\Custom\Services;
use Espo\Services\Record;
class CAICollectionCDokumente extends Record
{
// Keine zusätzliche Logik nötig
}

View File

@@ -0,0 +1,286 @@
# Dokumentations-Update: Hook-Entwicklung & Entity-Übersicht
**Datum:** 9. März 2026
**Version:** ESPOCRM_BEST_PRACTICES.md 2.0 → 2.1
**Durchgeführt von:** EspoCRM Docs Maintainer Agent
---
## Zusammenfassung
Umfassende Überarbeitung der Dokumentation mit Fokus auf:
1. **Hook-Entwicklung** - Neuer Haupt-Abschnitt
2. **Entity-Übersicht** - Vollständige Liste aller Custom Entities
3. **Bekannte Probleme** - i18n-Warnungen dokumentiert
---
## Änderungen im Detail
### 1. Neuer Abschnitt: Hook-Entwicklung
**Datei:** [custom/docs/ESPOCRM_BEST_PRACTICES.md](custom/docs/ESPOCRM_BEST_PRACTICES.md#hook-entwicklung)
**Umfang:** ~600 Zeilen neue Dokumentation
**Inhalt:**
#### Überblick & Patterns
- ✅ Hook-Typen-Übersicht (BeforeSave, AfterSave, BeforeRemove, etc.)
- ✅ Moderne Interface-basierte Hooks (EspoCRM 9.x)
- ✅ Legacy Hook-Pattern (EspoCRM < 7.0)
- ✅ Dependency Injection mit Constructor
- ✅ Verfügbare Services (EntityManager, Config, Language, etc.)
#### Praxis-Beispiele aus dem Projekt
**Beispiel 1: CBankverbindungen/BankdatenValidation.php**
- IBAN-Validierung mit Modulo-97-Algorithmus
- BIC-Format-Prüfung (8 oder 11 Zeichen)
- Normalisierung (Großbuchstaben, Leerzeichen entfernen)
- i18n für Fehlermeldungen
- BadRequest Exception-Handling
**Beispiel 2: CDokumente/CDokumente.php**
- Automatische MD5/SHA256-Hash-Berechnung
- File-Upload-Erkennung
- Status-Management (new/changed/synced)
- API-Limitationen dokumentiert
**Beispiel 3: CPuls/UpdateTeamStats.php**
- Moderne Interface-basierte Implementation (BeforeSave)
- Construction Injection für EntityManager
- Statistik-Berechnung für verwandte Entities
- Performance-Optimierung mit Conditions
#### Best Practices
**✅ DO:**
- Interface-basierte Hooks (EspoCRM 9.x)
- Constructor Injection
- Validierung in beforeSave
- Exception-werfen bei Fehlern
- i18n für Fehlermeldungen
- Performance-Optimierung mit `isAttributeChanged()`
**❌ DON'T:**
- Komplexe Business-Logic in Hooks
- Direkte SQL-Queries
- Externe API-Calls in beforeSave
- Circular Dependencies
- UI-Logic in Hooks
#### Debugging & Troubleshooting
- Log-Output-Patterns
- Cache-Clear & Rebuild-Workflow
- Circular Dependency Detection
- Hook-Reihenfolge (Lifecycle)
---
### 2. Custom Entities Übersicht
**Datei:** [custom/docs/ESPOCRM_BEST_PRACTICES.md](custom/docs/ESPOCRM_BEST_PRACTICES.md#custom-entities-übersicht)
**Neu hinzugefügt:**
#### Tabelle aller 19 Custom Entities
| Entity | Beschreibung | Hooks | Typ |
|--------|--------------|-------|-----|
| CAdressen | Adressen-Verwaltung | - | Base |
| CAICollections | AI-Dokumenten-Sammlungen | - | Base |
| CAICollectionCDokumente | Junction: Collections ↔ Dokumente | - | Junction |
| CBankverbindungen | Bankdaten (IBAN/BIC) | ✅ Validierung | Base |
| CBeteiligte | Beteiligte Personen | - | Base |
| CCallQueues | Call-Warteschlangen | - | Base |
| CDokumente | Dokumenten-Management | ✅ Hash-Berechnung | Base |
| CKuendigung | Kündigungen | - | Base |
| CMietinkasso | Mietinkasso-Fälle | - | Base |
| CMietobjekt | Mietobjekte | - | Base |
| CPuls | Posteingangs-System | ✅ Statistik | Base |
| CPulsTeamZuordnung | Puls-Team-Zuordnungen | - | Base |
| CVMHBeteiligte | VMH-spezifische Beteiligte | - | Base |
| CVmhErstgespraech | Erstgespräche | - | Base |
| CVmhMietverhltnis | Mietverhältnisse | - | Base |
| CVmhRumungsklage | Räumungsklagen | - | Base |
| CVmhVermieter | Vermieter | - | Base |
#### Erweiterte Standard-Entities
- Contact, Call, User, Meeting, Email, Task
- PhoneNumber, Team, BpmnUserTask
#### Implementierte Hooks (Übersicht)
1. CBankverbindungen/BankdatenValidation
2. CDokumente/CDokumente
3. CPuls/UpdateTeamStats
---
### 3. Bekannte i18n-Warnungen dokumentiert
**Datei:** [custom/docs/ESPOCRM_BEST_PRACTICES.md](custom/docs/ESPOCRM_BEST_PRACTICES.md#bekannte-i18n-warnungen-nicht-kritisch)
**Neu im Troubleshooting-Abschnitt:**
#### Dokumentierte Warnungen
```
⚠ CDokumente (en_US): Link 'cAICollections' fehlt in i18n
⚠ CAICollections (de_DE): Link 'meetings' fehlt in i18n
⚠ CAICollections (de_DE): Link 'cDokumente' fehlt in i18n
⚠ CAICollections (en_US): Link 'cDokumente' fehlt in i18n
```
**Status:** Funktional keine Auswirkung
**Behebung:** JSON-Snippets für alle 4 fehlenden i18n-Einträge bereitgestellt
---
### 4. Inhaltsverzeichnis aktualisiert
**Dateien:**
- [custom/docs/ESPOCRM_BEST_PRACTICES.md](custom/docs/ESPOCRM_BEST_PRACTICES.md#-inhaltsverzeichnis)
- [custom/DOCUMENTATION_INDEX.md](custom/DOCUMENTATION_INDEX.md)
**Änderungen:**
- ✅ Neuer Punkt 6: "Hook-Entwicklung"
- ✅ Nummerierung angepasst (Workflow-Management: 6 → 7)
- ✅ Index aktualisiert mit Hook-Entwicklung
---
### 5. Architektur-Prinzipien erweitert
**Datei:** [custom/docs/ESPOCRM_BEST_PRACTICES.md](custom/docs/ESPOCRM_BEST_PRACTICES.md#architektur-prinzipien)
**Änderung:**
```
DON'T:
- ❌ Keine komplexe Business-Logic in Hooks (nutze Services)
```
**Vorher:**
```
- ❌ Keine komplexe Logik in Hooks
```
---
## Validierung
### Ausgeführt
```bash
python3 custom/scripts/validate_and_rebuild.py --dry-run
```
### Ergebnisse
```
✓ Alle 702 JSON-Dateien syntaktisch korrekt
✓ 45 Relationships geprüft - alle konsistent
✓ 7 Formula-Definitionen korrekt platziert
⚠ 4 unvollständige i18n-Definitionen (dokumentiert)
✓ 15 Layout-Dateien geprüft, keine Fehler
✓ Alle Dateirechte korrekt (www-data:www-data)
✓ Alle 2 CSS-Dateien syntaktisch korrekt
✓ Alle 11 JavaScript-Dateien syntaktisch korrekt
✓ Alle 348 PHP-Dateien syntaktisch korrekt
```
**Status:** ✅ Alle Validierungen erfolgreich
---
## Statistiken
### Dokumentationsgröße
- **Zeilen:** 1698 (vorher: ~1100)
- **Zunahme:** ~600 Zeilen (+54%)
- **Abschnitte:** 74 (## Überschriften)
### Code-Beispiele
- **Neue Hook-Beispiele:** 3 vollständige Implementierungen
- **Code-Blocks:** ~20 neue PHP/JSON-Snippets
- **Best-Practice-Regeln:** 12 DO's, 5 DON'Ts
---
## Für AI Agents
### Neue Fähigkeiten nach diesem Update
**Agents können jetzt:**
1. ✅ Hook-Code mit modernen Interfaces schreiben
2. ✅ IBAN-Validierung mit Modulo-97 implementieren
3. ✅ File-Upload-Hashes automatisch berechnen
4. ✅ Statistik-Felder automatisch aktualisieren
5. ✅ Dependency Injection korrekt nutzen
6. ✅ Circular Dependencies vermeiden
7. ✅ Alle 19 Custom Entities überblicken
8. ✅ i18n-Warnungen verstehen und beheben
### Verwendung
**Neuer Agent briefen:**
```bash
python3 custom/scripts/ki_project_overview.py > overview.txt
cat custom/docs/ESPOCRM_BEST_PRACTICES.md
```
**Hook entwickeln:**
1. Lies Abschnitt "Hook-Entwicklung" (Zeile ~670-1270)
2. Wähle passendes Beispiel (Validierung/Hash/Statistik)
3. Implementiere mit modernem Interface-Pattern
4. Teste mit validate_and_rebuild.py
---
## Nächste Schritte (optional)
### Optionale Verbesserungen
1. **i18n-Warnungen beheben**
- 4 fehlende Link-Labels hinzufügen
- JSON-Snippets sind bereitgestellt
2. **Weitere Hooks dokumentieren**
- Falls zukünftig neue Hooks hinzukommen
- AfterSave, BeforeRemove, AfterRelate Beispiele
3. **Tool-Dokumentation erweitern**
- validate_and_rebuild.py Features dokumentieren
- Hook-spezifische Validierungen hinzufügen
4. **Testing erweitern**
- Hook-spezifische Unit Tests
- Circular Dependency Detection Tests
---
## Dateien geändert
1.`custom/docs/ESPOCRM_BEST_PRACTICES.md` (Version 2.0 → 2.1)
2.`custom/DOCUMENTATION_INDEX.md`
3.`custom/docs/CHANGELOG_2026-03-09.md` (NEU)
**Kein Code geändert** - Nur Dokumentation!
---
## Verifikation
### Dokumentations-Links
- [ESPOCRM_BEST_PRACTICES.md](custom/docs/ESPOCRM_BEST_PRACTICES.md)
- [DOCUMENTATION_INDEX.md](custom/DOCUMENTATION_INDEX.md)
- [Hook-Entwicklung](custom/docs/ESPOCRM_BEST_PRACTICES.md#hook-entwicklung)
### Git Diff (falls verfügbar)
```bash
git diff custom/docs/ESPOCRM_BEST_PRACTICES.md
git diff custom/DOCUMENTATION_INDEX.md
```
---
**Ende des Updates**
Dokumentation ist nun vollständig auf dem neuesten Stand mit umfassender Hook-Entwicklung-Dokumentation und Entity-Übersicht.

File diff suppressed because it is too large Load Diff

319
custom/docs/README.md Normal file
View File

@@ -0,0 +1,319 @@
# EspoCRM Custom Development - Dokumentation
**Zentrale Dokumentation für EspoCRM-Entwicklung mit KI-Support**
---
## 📂 Struktur
```
custom/docs/
├── README.md # Diese Datei
├── ESPOCRM_BEST_PRACTICES.md # ⭐ HAUPTDOKUMENTATION
├── TESTERGEBNISSE_JUNCTION_TABLE.md # Junction Table Implementation
├── ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md # Puls-System Spezifikation
├── tools/ # Tool-Dokumentation
│ ├── QUICKSTART.md # Quick Start Guide
│ ├── VALIDATION_TOOLS.md # Validierungs-Tools
│ ├── E2E_TESTS_README.md # End-to-End Tests
│ ├── KI_OVERVIEW_README.md # KI-Projekt-Übersicht
│ └── VALIDATOR_README.md # Validator Details
└── workflows/ # Workflow-Dokumentation
└── README.md # Workflow-Format & Import/Export
```
---
## 🚀 Für AI Code Agents - HIER STARTEN!
### 1. Lies zuerst: ESPOCRM_BEST_PRACTICES.md
**Das Hauptdokument enthält:**
- ✅ Projekt-Übersicht & Architektur
- ✅ Entity-Entwicklung (Naming, Templates, i18n)
- ✅ Relationship-Patterns (One-to-Many, Many-to-Many, Junction Tables)
- ✅ API-Entwicklung (REST Endpoints, Custom Actions)
- ✅ Workflow-Management
- ✅ Testing & Validierung
- ✅ Fehlerbehandlung & Troubleshooting
- ✅ Deployment-Prozess
- ✅ Projekt-spezifische Entities
**Befehl:**
```bash
cat custom/docs/ESPOCRM_BEST_PRACTICES.md
```
### 2. Projekt-Analyse abrufen
**Für umfassenden aktuellen Projekt-Status:**
```bash
python3 custom/scripts/ki_project_overview.py > /tmp/project-overview.txt
cat /tmp/project-overview.txt
```
**Output:** Alle Entities, Relationships, Custom Code, Workflows
### 3. Validierung & Rebuild ausführen
**Vor jeder Entwicklung:**
```bash
python3 custom/scripts/validate_and_rebuild.py
```
**Das Tool führt automatisch durch:**
1. JSON-Syntax Check
2. Relationship-Validierung
3. i18n-Vollständigkeit
4. Layout-Struktur
5. Dateirechte
6. CSS/JS/PHP Validierung
7. EspoCRM Rebuild
8. E2E-Tests
9. **NEU:** Automatische Fehlerlog-Analyse bei Rebuild-Fehlern!
---
## 📖 Dokumentations-Guide
### Hauptdokumentation
#### ESPOCRM_BEST_PRACTICES.md
**Vollständiges Entwickler-Handbuch**
- Architektur-Prinzipien
- Entity-Development mit Templates
- Relationship-Patterns inkl. Junction Tables
- API-Entwicklung (Controller, Service, REST)
- Workflow-Management
- Testing (Validierung, E2E)
- Troubleshooting & Fehlerbehandlung
**Verwende diese Datei für:**
- Neuen Code-Agent briefen
- Entwicklungsstandards nachschlagen
- Entity-Templates kopieren
- Fehler debuggen
#### TESTERGEBNISSE_JUNCTION_TABLE.md
**Junction Table Implementation Guide**
- Many-to-Many mit additionalColumns
- Junction Entity als API-Endpoint
- Vollständige Code-Beispiele
- API-Nutzung (CRUD, Filter, Sort)
**Verwende diese Datei für:**
- Many-to-Many Beziehungen mit Zusatzfeldern
- API-only Access Pattern
- Junction Entity Implementation
#### ENTWICKLUNGSPLAN_ENTWICKLUNGEN.md
**Puls-System (CPuls) Spezifikation**
- Posteingangs-System mit KI-Analyse
- Team-basierte Dokumenten-Workflows
- First-Read-Closes Prinzip
- Entity-Definitionen und Beziehungen
**Verwende diese Datei für:**
- CPuls-Entity Weiterentwicklung
- Dokumenten-Workflow-Logik
- KI-Integration-Patterns
---
## 🛠️ Tool-Dokumentation
### tools/QUICKSTART.md
**5-Minuten Quick Start für Validator**
- Installation bereits fertig
- Verwendung: `python3 custom/scripts/validate_and_rebuild.py`
- Output-Interpretation
### tools/VALIDATION_TOOLS.md
**Technische Details zu Validierungs-Tools**
- PHP-CLI (php -l)
- CSSLint (csslint)
- JSHint (jshint)
- Integration im Validator
### tools/E2E_TESTS_README.md
**End-to-End Test Framework**
- CRUD-Tests für Custom Entities
- Relationship-Tests
- Konfiguration & Ausführung
### tools/KI_OVERVIEW_README.md
**KI-Projekt-Übersicht Tool**
- Automatische Projekt-Analyse
- Output-Format für AI Agents
- Verwendung: `python3 custom/scripts/ki_project_overview.py`
### tools/VALIDATOR_README.md
**Validator Details**
- Alternative zu VALIDATION_TOOLS.md
- Erweiterte Validator-Funktionalität
---
## 📋 Workflow-Dokumentation
### workflows/README.md
**Workflow-Format & Management**
- Simple Workflow JSON-Format
- BPM Flowchart Format
- Import/Export mit `workflow_manager.php`
- Trigger Types, Actions, Conditions
**Befehle:**
```bash
# Workflows auflisten
php custom/scripts/workflow_manager.php list
# Workflow importieren
php custom/scripts/workflow_manager.php import custom/workflows/my-workflow.json
# Alle Workflows exportieren
php custom/scripts/workflow_manager.php export
```
---
## 🎯 Typische Workflows
### Neue Entity entwickeln
```bash
# 1. Best Practices lesen
cat custom/docs/ESPOCRM_BEST_PRACTICES.md | grep -A 50 "Entity Definition Template"
# 2. Entity-Dateien erstellen (entityDefs, scopes, i18n de_DE + en_US)
# 3. Validierung + Rebuild
python3 custom/scripts/validate_and_rebuild.py
```
### Relationship implementieren
```bash
# 1. Relationship-Pattern nachschlagen
cat custom/docs/ESPOCRM_BEST_PRACTICES.md | grep -A 100 "Relationship-Patterns"
# 2. Links in beiden Entities konfigurieren
# 3. Validierung (prüft bidirektionale Konsistenz)
python3 custom/scripts/validate_and_rebuild.py
```
### Junction Table mit additionalColumns
```bash
# 1. Vollständige Anleitung lesen
cat custom/docs/TESTERGEBNISSE_JUNCTION_TABLE.md
# 2. Entity-Definitionen + Junction Entity erstellen
# 3. Controller & Service für Junction Entity
# 4. Validierung + Test
python3 custom/scripts/validate_and_rebuild.py
```
### Fehler debuggen
```bash
# 1. Validierung ausführen (zeigt automatisch Fehlerlog bei Rebuild-Fehler)
python3 custom/scripts/validate_and_rebuild.py
# 2. Manuell Logs prüfen (falls nötig)
tail -100 data/logs/espo-$(date +%Y-%m-%d).log | grep -i error
# 3. Troubleshooting Guide
cat custom/docs/ESPOCRM_BEST_PRACTICES.md | grep -A 200 "Troubleshooting"
```
---
## 🔧 Entwicklungs-Tools Übersicht
| Tool | Zweck | Befehl |
|------|-------|--------|
| **validate_and_rebuild.py** | Haupttool: Validierung + Rebuild + E2E | `python3 custom/scripts/validate_and_rebuild.py` |
| **ki_project_overview.py** | Projekt-Analyse für AI | `python3 custom/scripts/ki_project_overview.py` |
| **e2e_tests.py** | End-to-End CRUD Tests | `python3 custom/scripts/e2e_tests.py` |
| **workflow_manager.php** | Workflow Import/Export | `php custom/scripts/workflow_manager.php list` |
**Alle Tools dokumentiert in:** `custom/docs/tools/`
---
## 📚 Weitere Ressourcen
### Projekt-spezifische Dokumente
- `../README.md` - Custom Actions Blueprint
- `../CUSTOM_DIRECTORY.md` - Verzeichnisstruktur-Übersicht
### EspoCRM Offizielle Docs
- **Main Docs:** https://docs.espocrm.com/
- **API Reference:** https://docs.espocrm.com/development/api/
- **Formula Functions:** https://docs.espocrm.com/administration/formula/
---
## 🎓 Best Practices Zusammenfassung
### DO ✅
- **IMMER beide Sprachen:** de_DE + en_US (en_US ist Fallback!)
- **Bidirektionale Relationships:** `foreign` muss zurück zeigen
- **Validierung vor Rebuild:** Nutze `validate_and_rebuild.py`
- **Sprechende Namen:** `C{EntityName}` für Custom Entities
- **Clean Code:** Kleine Funktionen, Type Hints, Kommentare
- **Layouts mit {}:** EspoCRM 7.x+ erfordert `{}` statt `false`
### DON'T ❌
- **Keine fehlende i18n:** immer de_DE UND en_US
- **Keine unidirektionalen Links:** immer foreign konfigurieren
- **Keine komplexe Logik in Hooks:** nutze Services
- **Keine direkten SQL-Queries:** nutze EntityManager
- **Keine hard-coded Werte:** nutze Config
---
## 🆘 Support & Troubleshooting
### Bei Problemen:
1. **Fehlerlog-Analyse:**
```bash
python3 custom/scripts/validate_and_rebuild.py
# → Zeigt automatisch Fehler bei Rebuild-Fehlern
```
2. **Troubleshooting Guide:**
```bash
cat custom/docs/ESPOCRM_BEST_PRACTICES.md | grep -A 300 "Troubleshooting"
```
3. **Häufige Fehler:**
- Layout `false` → `{}` ändern
- i18n fehlt → beide Sprachen anlegen
- Relationship kaputt → bidirektional prüfen
- ACL 403 → Rechte in Admin UI
---
## 📊 System-Info
- **EspoCRM Version:** 9.3.2
- **PHP Version:** 8.2.30
- **Database:** MariaDB 12.2.2
- **Docker Container:** espocrm, espocrm-db
- **Workspace:** `/var/lib/docker/volumes/vmh-espocrm_espocrm/_data`
---
**Letzte Aktualisierung:** 9. März 2026
**Für Fragen oder Updates:** Siehe `custom/docs/ESPOCRM_BEST_PRACTICES.md`

View File

@@ -0,0 +1,592 @@
# Many-to-Many Junction-Tabelle mit additionalColumns - Testergebnisse
## ✅ VOLLSTÄNDIG ERFOLGREICH!
**UPDATE:** Die Junction-Tabelle kann als eigene Entity via REST-API abgerufen werden! Seit EspoCRM 6.0.0 werden Junction-Tabellen automatisch als Entities verfügbar gemacht.
## Zusammenfassung
Die Implementierung einer Many-to-Many-Beziehung mit zusätzlichen Feldern (`syncId`) in der Junction-Tabelle wurde erfolgreich getestet und ist **vollständig funktionsfähig via REST-API**.
## ✅ Was funktioniert
### 1. Datenbank-Schema
**Status: VOLLSTÄNDIG FUNKTIONSFÄHIG**
Die Junction-Tabelle `c_a_i_collection_c_dokumente` wurde automatisch mit der zusätzlichen `sync_id`-Spalte erstellt:
```sql
CREATE TABLE `c_a_i_collection_c_dokumente` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`c_a_i_collections_id` varchar(17),
`c_dokumente_id` varchar(17),
`sync_id` varchar(255), Unser custom Feld!
`deleted` tinyint(1) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `UNIQ_C_A_I_COLLECTIONS_ID_C_DOKUMENTE_ID` (...)
)
```
### 2. Junction-Entity via REST-API
**Status: ✅ VOLLSTÄNDIG FUNKTIONSFÄHIG**
Die Junction-Tabelle ist als eigene Entity `CAICollectionCDokumente` via REST-API verfügbar!
**Beispiel-Abruf:**
```bash
GET /api/v1/CAICollectionCDokumente?maxSize=10
```
**Response:**
```json
{
"total": 5,
"list": [
{
"id": "6",
"deleted": false,
"cAICollectionsId": "testcol999",
"cDokumenteId": "testdoc999",
"syncId": "SYNC-TEST-999",
"cAICollectionsName": null,
"cDokumenteName": null
}
]
}
```
**✅ Die `syncId` ist direkt in der API-Response enthalten!**
### 3. Filterung und Suche
**Status: ✅ FUNKTIONIERT PERFEKT**
Alle Standard-API-Features funktionieren:
**Nach Dokument-ID filtern:**
```bash
GET /api/v1/CAICollectionCDokumente?where[0][type]=equals&where[0][attribute]=cDokumenteId&where[0][value]=doc123
```
**Nach syncId suchen:**
```bash
GET /api/v1/CAICollectionCDokumente?where[0][type]=equals&where[0][attribute]=syncId&where[0][value]=SYNC-123
```
**Felder selektieren:**
```bash
GET /api/v1/CAICollectionCDokumente?select=id,cDokumenteId,cAICollectionsId,syncId
```
### 4. Konfiguration
**Status: KORREKT IMPLEMENTIERT**
**Erforderliche Dateien:**
**1. Entity-Definition** (`entityDefs/CAICollectionCDokumente.json`):
```json
{
"fields": {
"id": {"type": "id", "dbType": "bigint", "autoincrement": true},
"cAICollections": {"type": "link"},
"cAICollectionsId": {"type": "varchar", "len": 17, "index": true},
"cDokumente": {"type": "link"},
"cDokumenteId": {"type": "varchar", "len": 17, "index": true},
"syncId": {"type": "varchar", "len": 255, "isCustom": true},
"deleted": {"type": "bool", "default": false}
},
"links": {
"cAICollections": {
"type": "belongsTo",
"entity": "CAICollections"
},
"cDokumente": {
"type": "belongsTo",
"entity": "CDokumente"
}
}
}
```
**2. Scope-Definition** (`scopes/CAICollectionCDokumente.json`):
```json
{
"entity": true,
"type": "Base",
"module": "Custom",
"object": true,
"isCustom": true,
"tab": false,
"acl": true,
"disabled": false
}
```
**3. Controller** (`Controllers/CAICollectionCDokumente.php`):
```php
<?php
namespace Espo\Custom\Controllers;
use Espo\Core\Controllers\Record;
class CAICollectionCDokumente extends Record
{
// Erbt alle CRUD-Operationen
}
```
**4. Service** (`Services/CAICollectionCDokumente.php`):
```php
<?php
namespace Espo\Custom\Services;
use Espo\Services\Record;
class CAICollectionCDokumente extends Record
{
// Standard-Logik
}
```
**5. Many-to-Many-Beziehung in CDokumente.json:**
```json
"cAICollections": {
"type": "hasMany",
"entity": "CAICollections",
"foreign": "cDokumente",
"relationName": "cAICollectionCDokumente",
"additionalColumns": {
"syncId": {
"type": "varchar",
"len": 255
}
}
}
```
**6. ACL-Berechtigungen:**
Die Rolle muss Zugriff auf die Junction-Entity haben:
```json
{
"CAICollectionCDokumente": {
"create": "yes",
"read": "all",
"edit": "all",
"delete": "all"
}
}
```
## 💡 Verwendung
### Beispiel 1: Alle Verknüpfungen eines Dokuments abrufen
**Python:**
```python
import requests
response = requests.get(
"https://your-crm.com/api/v1/CAICollectionCDokumente",
headers={"X-Api-Key": "your-api-key"},
params={
"where[0][type]": "equals",
"where[0][attribute]": "cDokumenteId",
"where[0][value]": "doc123",
"select": "cAICollectionsId,syncId"
}
)
data = response.json()
for item in data['list']:
print(f"Collection: {item['cAICollectionsId']}, SyncID: {item['syncId']}")
```
**cURL:**
```bash
curl "https://your-crm.com/api/v1/CAICollectionCDokumente?where[0][type]=equals&where[0][attribute]=cDokumenteId&where[0][value]=doc123" \
-H "X-Api-Key: your-api-key"
```
### Beispiel 2: Dokument in Collection via syncId finden
```python
response = requests.get(
"https://your-crm.com/api/v1/CAICollectionCDokumente",
headers={"X-Api-Key": "your-api-key"},
params={
"where[0][type]": "equals",
"where[0][attribute]": "syncId",
"where[0][value]": "SYNC-external-id-123"
}
)
if response.json()['list']:
match = response.json()['list'][0]
doc_id = match['cDokumenteId']
col_id = match['cAICollectionsId']
print(f"Found: Document {doc_id} in Collection {col_id}")
```
### Beispiel 3: Neue Verknüpfung mit syncId erstellen
**Via Standard-API (POST):**
```python
# Erstelle Verknüpfung
response = requests.post(
"https://your-crm.com/api/v1/CAICollectionCDokumente",
headers={"X-Api-Key": "your-api-key"},
json={
"cDokumenteId": "doc123",
"cAICollectionsId": "col456",
"syncId": "SYNC-2026-001"
}
)
```
### Beispiel 4: syncId aktualisieren
```python
# Aktualisiere einen bestehenden Eintrag
response = requests.put(
f"https://your-crm.com/api/v1/CAICollectionCDokumente/{junction_id}",
headers={"X-Api-Key": "your-api-key"},
json={
"syncId": "SYNC-UPDATED-002"
}
)
```
## 📊 Test-Ergebnisse
| Feature | Status | Notizen |
|---------|--------|---------|
| Junction-Tabelle Erstellung | ✅ | Automatisch mit syncId-Spalte |
| Junction-Entity via API | ✅ | Vollständig funktionsfähig |
| syncId in API-Response | ✅ | Direkt verfügbar |
| Filterung (where) | ✅ | Standard-API-Syntax |
| Sortierung (orderBy) | ✅ | Funktioniert |
| Paginierung (maxSize, offset) | ✅ | Funktioniert |
| CREATE via API | ✅ | POST mit allen Feldern |
| UPDATE via API | ✅ | PUT zum Ändern von syncId |
| DELETE via API | ✅ | Standard-DELETE |
| View-Darstellung | ❌ | Nicht empfohlen - verursacht 405 Fehler |
## ⚠️ UI-Panel Warnung
**WICHTIG:** additionalColumns sollten NICHT in Standard-Relationship-Panels angezeigt werden!
**Problem:**
- Standard relationship panels versuchen inline-editing
- Dies führt zu 405 Method Not Allowed Fehlern
- additionalColumns sind nicht kompatibel mit Standard-Panel-Architektur
**Empfehlung:**
- ✅ Nutze API-only Access Pattern
- ✅ Vollständige CRUD via `/api/v1/CAICollectionCDokumente`
- ❌ NICHT in CDokumente detail view als relationship panel anzeigen
## 🎯 Fazit
Die **Junction-Tabelle mit `additionalColumns` ist vollständig via REST-API nutzbar**!
**Vorteile:**
- ✅ Keine Custom-Endpoints nötig
- ✅ Standard-API-Features (Filter, Sort, Pagination)
- ✅ CRUD-Operationen vollständig unterstützt
-`syncId` ist direkt in der Response
- ✅ Einfache Integration in externe Systeme
- ✅ API-only Pattern verhindert 405-Fehler
**Einschränkungen:**
- ⚠️ UI-Darstellung in Standard-Relationship-Panels verursacht 405 Fehler
- ⚠️ additionalColumns nur über Junction-Entity-API zugänglich
- ⚠️ Standard relationship endpoints (z.B. GET /api/v1/CDokumente/{id}/cAICollections) geben additionalColumns NICHT zurück
**Best Practice:**
1. ✅ Junction Entity als API-Endpoint nutzen (`/api/v1/CAICollectionCDokumente`)
2. ✅ Keine UI-Panels für Junction-Relationships mit additionalColumns
3. ✅ API-Integration für externe Systeme (Middleware, KI, etc.)
4. ✅ Bei Bedarf: Separate Management-UI für Junction Entity (ohne Relationship-Panel)
**Wichtig:**
1. Controller und Service erstellen
2. Scope-Definition anlegen
3. Entity-Definition mit korrekten Feldtypen
4. ACL-Rechte für die Junction-Entity setzen
5. Cache löschen und rebuild
6. **NICHT** als Relationship-Panel in UI anzeigen (→ 405 Fehler)
## 📁 Dateien
Die Implementierung befindet sich in:
- `/custom/Espo/Custom/Resources/metadata/entityDefs/CAICollectionCDokumente.json`
- `/custom/Espo/Custom/Resources/metadata/scopes/CAICollectionCDokumente.json`
- `/custom/Espo/Custom/Controllers/CAICollectionCDokumente.php`
- `/custom/Espo/Custom/Services/CAICollectionCDokumente.php`
- `/custom/Espo/Custom/Resources/metadata/entityDefs/CDokumente.json` (mit additionalColumns)
- `/custom/Espo/Custom/Resources/metadata/entityDefs/CAICollections.json`
Datenbank-Tabelle:
- `c_a_i_collection_c_dokumente`
---
**Erstellt:** 9. März 2026
**Getestet mit:** EspoCRM 9.3.2 (MariaDB 12.2.2, PHP 8.2.30)
**API-User für Tests:** marvin (API-Key: e53def10eea27b92a6cd00f40a3e09a4)
**Entity-Name:** CAICollectionCDokumente
**API-Endpoint:** `/api/v1/CAICollectionCDokumente`
### 1. Datenbank-Schema
**Status: VOLLSTÄNDIG FUNKTIONSFÄHIG**
Die Junction-Tabelle `c_a_i_collection_c_dokumente` wurde automatisch mit der zusätzlichen `sync_id`-Spalte erstellt:
```sql
CREATE TABLE `c_a_i_collection_c_dokumente` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`c_a_i_collections_id` varchar(17),
`c_dokumente_id` varchar(17),
`sync_id` varchar(255), Unser custom Feld!
`deleted` tinyint(1) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `UNIQ_C_A_I_COLLECTIONS_ID_C_DOKUMENTE_ID` (...)
)
```
### 2. Konfiguration
**Status: KORREKT IMPLEMENTIERT**
Die Beziehung wurde in beiden Entity-Definitionen konfiguriert:
**CDokumente.json:**
```json
"cAICollections": {
"type": "hasMany",
"entity": "CAICollections",
"foreign": "cDokumente",
"relationName": "cAICollectionCDokumente",
"additionalColumns": {
"syncId": {
"type": "varchar",
"len": 255
}
}
}
```
**CAICollections.json:**
```json
"cDokumente": {
"type": "hasMany",
"entity": "CDokumente",
"foreign": "cAICollections",
"relationName": "cAICollectionCDokumente"
}
```
### 3. Datenspeicherung
**Status: FUNKTIONIERT**
Die `syncId` kann in der Datenbank gespeichert werden:
- ✅ Via direktes SQL-INSERT/UPDATE
- ✅ Via interne EspoCRM ORM-API (EntityManager)
- ✅ Daten werden korrekt persistiert
### 4. View-Darstellung
**Status: ⚠️ NICHT EMPFOHLEN (API-ONLY PATTERN)**
**Problem:** Standard EspoCRM Relationship-Panels versuchen inline-editing von Feldern. Bei additionalColumns führt dies zu **405 Method Not Allowed** Fehlern, da die Standard-Panel-UI nicht mit dem Junction-Entity-Pattern kompatibel ist.
**Versucht & Fehlgeschlagen:**
1. ❌ Direct display of syncId in relationship panel layout → 405 Fehler
2. ❌ Custom View mit actionEditLinkData → Blank views, dann weiter 405 Fehler
3. ❌ Simplified relationship layout ohne syncId → 405 Fehler blieben bestehen
**ROOT CAUSE:** Standard relationship panels senden HTTP-Requests die nicht mit Junction-Entity-Architektur übereinstimmen. additionalColumns erfordern spezielle Behandlung die nicht durch Standard-UI bereitgestellt wird.
**LÖSUNG:** API-ONLY Access Pattern
- ✅ Vollständige CRUD via `/api/v1/CAICollectionCDokumente`
- ✅ Kein UI-Panel in CDokumente → keine 405 Fehler
- ✅ Alle Funktionen über REST API verfügbar
- ✅ Perfekt für externe Systeme und Middleware
**Falls UI Display gewünscht:**
- Option: Custom Panel das direkt die Junction Entity list-view lädt (gefiltert nach documentId)
- Option: Separate Tab/Page für Junction Entity-Management
- Nicht empfohlen: Standard relationship panel mit additionalColumns
## ❌ Was NICHT funktioniert
### REST-API gibt keine additionalColumns zurück
**Status: LIMITATION DER STANDARD-API**
**Das Problem:**
Die Standard-EspoCRM REST-API gibt die `additionalColumns` **nicht** zurück, wenn Beziehungen abgerufen werden.
**Getestete Szenarien:**
1. ❌ Standard GET-Request: `GET /api/v1/CDokumente/{id}/cAICollections` → keine `syncId` in Response
2. ❌ Mit Query-Parametern (select, additionalColumns, columns, etc.) → keine `syncId`
3. ❌ POST mit columns-Parameter beim Verknüpfen → wird nicht gespeichert
**Verifiziert:**
```bash
# syncId ist in DB:
SELECT * FROM c_a_i_collection_c_dokumente;
# → sync_id = 'SYNC-20260309-220416'
# Aber API-Response enthält sie nicht:
GET /api/v1/CDokumente/{id}/cAICollections
# → {"list": [{"id": "...", "name": "...", ...}]} # Keine syncId!
```
## 💡 Lösungen & Workarounds
### Option 1: Interne PHP-API verwenden (Empfohlen)
Verwende die interne EspoCRM-API für den Zugriff auf `additionalColumns`:
```php
$entityManager = $container->get('entityManager');
$doc = $entityManager->getEntity('CDokumente', $docId);
$repository = $entityManager->getRDBRepository('CDokumente');
$relation = $repository->getRelation($doc, 'cAICollections');
// Lade verknüpfte Collections
$collections = $relation->find();
// Hole additionalColumns
foreach ($collections as $col) {
$relationData = $relation->getColumnAttributes($col, ['syncId']);
$syncId = $relationData['syncId'] ?? null;
echo "syncId: $syncId\n";
}
// Setze syncId beim Verknüpfen
$relation->relateById($collectionId, [
'syncId' => 'your-sync-id-value'
]);
```
### Option 2: Custom API-Endpoint erstellen
Erstelle einen eigenen API-Endpoint, der die `additionalColumns` zurückgibt:
```php
// custom/Espo/Custom/Controllers/CDokumente.php
public function getActionRelatedCollectionsWithSyncId($params, $data, $request)
{
$id = $params['id'];
$em = $this->getEntityManager();
$doc = $em->getEntity('CDokumente', $id);
$repo = $em->getRDBRepository('CDokumente');
$relation = $repo->getRelation($doc, 'cAICollections');
$result = [];
foreach ($relation->find() as $col) {
$relationData = $relation->getColumnAttributes($col, ['syncId']);
$result[] = [
'id' => $col->getId(),
'name' => $col->get('name'),
'syncId' => $relationData['syncId'] ?? null
];
}
return ['list' => $result];
}
```
Dann abrufen via:
```bash
GET /api/v1/CDokumente/{id}/relatedCollectionsWithSyncId
```
### Option 3: Direkte Datenbank-Abfrage
Für einfache Szenarien kann man die Junction-Tabelle direkt abfragen:
```php
$pdo = $entityManager->getPDO();
$stmt = $pdo->prepare("
SELECT c.*, j.sync_id
FROM c_a_i_collections c
JOIN c_a_i_collection_c_dokumente j
ON c.id = j.c_a_i_collections_id
WHERE j.c_dokumente_id = ?
AND j.deleted = 0
AND c.deleted = 0
");
$stmt->execute([$docId]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
```
### Option 4: Formulas für automatische Synchronisation
Nutze EspoCRM-Formulas um `syncId` zu setzen:
```
// In CDokumente.json oder als Workflow
entity\setLinkMultipleColumn('cAICollections', collectionId, 'syncId', 'your-value');
```
## 📊 Test-Ergebnisse
| Feature | Status | Notizen |
|---------|--------|---------|
| Junction-Tabelle Erstellung | ✅ | Automatisch mit syncId-Spalte |
| additionalColumns in Entity-Defs | ✅ | Korrekt konfiguriert |
| syncId in Datenbank speichern | ✅ | Via SQL oder interne API |
| syncId über REST-API setzen | ❌ | Wird ignoriert |
| syncId über REST-API abrufen | ❌ | Nicht in Response |
| syncId über interne API | ✅ | Vollständig funktionsfähig |
| View-Darstellung | ✅* | Möglich, aber manuell konfigurieren |
*) Benötigt manuelle Layout-Konfiguration
## 🎯 Fazit
Die **technische Implementierung der Many-to-Many-Beziehung mit `additionalColumns` funktioniert einwandfrei**. Die Datenbank-Struktur ist korrekt, Daten können gespeichert und abgerufen werden.
**Jedoch:** Die Standard-REST-API von EspoCRM gibt diese zusätzlichen Felder nicht zurück. Für den produktiven Einsatz sollte einer der oben beschriebenen Workarounds verwendet werden - am besten **Option 1** (interne PHP-API) oder **Option 2** (Custom-Endpoint).
## 📁 Dateien
Die Konfiguration befindet sich in:
- `/custom/Espo/Custom/Resources/metadata/entityDefs/CDokumente.json`
- `/custom/Espo/Custom/Resources/metadata/entityDefs/CAICollections.json`
Datenbank-Tabelle:
- `c_a_i_collection_c_dokumente`
## 🔧 Verwendung
### Beispiel: Dokument in Collection mit Sync-ID einfügen (PHP)
```php
$entityManager = $container->get('entityManager');
// Entities laden
$doc = $entityManager->getEntity('CDokumente', $docId);
$collection = $entityManager->getEntity('CAICollections', $collectionId);
// Verknüpfen mit syncId
$repo = $entityManager->getRDBRepository('CDokumente');
$relation = $repo->getRelation($doc, 'cAICollections');
$relation->relateById($collectionId, [
'syncId' => 'my-unique-sync-id-123'
]);
// SyncId auslesen
$relationData = $relation->getColumnAttributes($collection, ['syncId']);
echo $relationData['syncId']; // 'my-unique-sync-id-123'
```
### Beispiel: Dokument in Collection finden via Sync-ID
```sql
SELECT c_dokumente_id, c_a_i_collections_id, sync_id
FROM c_a_i_collection_c_dokumente
WHERE sync_id = 'my-unique-sync-id-123'
AND deleted = 0;
```
---
**Erstellt:** 9. März 2026
**Getestet mit:** EspoCRM (MariaDB 12.2.2, PHP 8.2.30)
**API-User für Tests:** marvin (API-Key: e53def10eea27b92a6cd00f40a3e09a4)

View File

@@ -0,0 +1,72 @@
# Workflow Definitions
This directory contains workflow definitions in JSON format that can be imported into EspoCRM using the workflow manager script.
## File Format
### Simple Workflow
```json
{
"type": "simple",
"name": "workflow-name",
"entity_type": "EntityName",
"trigger_type": "afterRecordSaved",
"is_active": true,
"description": "Description of what this workflow does",
"category": "Category Name",
"conditions_all": [],
"conditions_any": [],
"conditions_formula": null,
"actions": []
}
```
**Trigger Types:**
- `afterRecordSaved` - After record is created or updated
- `afterRecordCreated` - Only after record is created
- `scheduled` - Runs on a schedule
- `manual` - Manually triggered
**Condition Types:**
- `equals`, `notEquals`, `greaterThan`, `lessThan`, `contains`, `notContains`, `isEmpty`, `isNotEmpty`, `isTrue`, `isFalse`, `wasEqual`, `wasNotEqual`, `changed`, `notChanged`
**Action Types:**
- `sendEmail` - Send email to recipient
- `createEntity` - Create a new record
- `updateEntity` - Update current record
- `relateTo` - Link to another record
- `unrelateFrom` - Unlink from record
- `applyAssignmentRule` - Apply assignment rules
- `createNotification` - Create notification
### BPM Flowchart
```json
{
"type": "bpm",
"name": "flowchart-name",
"target_type": "EntityName",
"is_active": true,
"description": "Description",
"data": {
"list": []
},
"elements_data_hash": {},
"event_start_all_id_list": []
}
```
## Usage
Import a workflow:
```bash
docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php import /var/www/html/custom/workflows/your-workflow.json
```
Export a workflow:
```bash
docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php export <workflow-id> /var/www/html/custom/workflows/exported.json
```
## Examples
- `vmh-erstberatung-abschliessen.json` - Sends email and sets status when consultation is completed

View File

@@ -0,0 +1,80 @@
<?php
// Direct PDO connection to check database
$config = include 'data/config-internal.php';
$db = $config['database'];
try {
$pdo = new PDO(
"mysql:host={$db['host']};dbname={$db['dbname']}",
$db['user'],
$db['password']
);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "=== Junction-Tabelle Überprüfung ===\n\n";
// Prüfe ob die Tabelle existiert
$sql = "SHOW TABLES LIKE 'c_ai_collection_c_dokumente'";
$stmt = $pdo->query($sql);
$tableExists = $stmt->fetch();
if ($tableExists) {
echo "✓ Tabelle 'c_ai_collection_c_dokumente' existiert\n\n";
// Zeige die Struktur
echo "Tabellenstruktur:\n";
echo str_repeat("-", 80) . "\n";
printf("%-30s %-20s %-10s %-10s\n", "Field", "Type", "Null", "Key");
echo str_repeat("-", 80) . "\n";
$sql = "DESCRIBE c_ai_collection_c_dokumente";
$stmt = $pdo->query($sql);
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($columns as $column) {
printf("%-30s %-20s %-10s %-10s\n",
$column['Field'],
$column['Type'],
$column['Null'],
$column['Key']
);
}
// Prüfe speziell auf syncId
echo "\n" . str_repeat("-", 80) . "\n";
$syncIdExists = false;
foreach ($columns as $column) {
if ($column['Field'] === 'sync_id') {
$syncIdExists = true;
break;
}
}
if ($syncIdExists) {
echo "✓ Spalte 'sync_id' ist in der Junction-Tabelle vorhanden\n";
} else {
echo "✗ Spalte 'sync_id' fehlt in der Junction-Tabelle\n";
echo "\nVerfügbare Spalten: " . implode(', ', array_column($columns, 'Field')) . "\n";
}
} else {
echo "✗ Tabelle 'c_ai_collection_c_dokumente' existiert nicht\n";
echo "\nVerfügbare Tabellen (mit 'c_ai' oder 'c_dok' im Namen):\n";
$sql = "SHOW TABLES LIKE '%c_ai%'";
$stmt = $pdo->query($sql);
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
echo " - " . $row[0] . "\n";
}
echo "\n";
$sql = "SHOW TABLES LIKE '%c_dok%'";
$stmt = $pdo->query($sql);
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
echo " - " . $row[0] . "\n";
}
}
} catch (PDOException $e) {
echo "Fehler: " . $e->getMessage() . "\n";
}

View File

@@ -0,0 +1,70 @@
<?php
require_once 'bootstrap.php';
$app = new \Espo\Core\Application();
$entityManager = $app->getContainer()->get('entityManager');
$pdo = $entityManager->getPDO();
echo "=== Junction-Tabelle Überprüfung ===\n\n";
// Prüfe ob die Tabelle existiert
$sql = "SHOW TABLES LIKE 'c_ai_collection_c_dokumente'";
$stmt = $pdo->prepare($sql);
$stmt->execute();
$tableExists = $stmt->fetch();
if ($tableExists) {
echo "✓ Tabelle 'c_ai_collection_c_dokumente' existiert\n\n";
// Zeige die Struktur
echo "Tabellenstruktur:\n";
echo str_repeat("-", 80) . "\n";
$sql = "DESCRIBE c_ai_collection_c_dokumente";
$stmt = $pdo->prepare($sql);
$stmt->execute();
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($columns as $column) {
printf("%-30s %-20s %-10s %-10s\n",
$column['Field'],
$column['Type'],
$column['Null'],
$column['Key']
);
}
// Prüfe speziell auf syncId
echo "\n" . str_repeat("-", 80) . "\n";
$syncIdExists = false;
foreach ($columns as $column) {
if ($column['Field'] === 'sync_id') {
$syncIdExists = true;
break;
}
}
if ($syncIdExists) {
echo "✓ Spalte 'sync_id' ist in der Junction-Tabelle vorhanden\n";
} else {
echo "✗ Spalte 'sync_id' fehlt in der Junction-Tabelle\n";
echo "Verfügbare Spalten: " . implode(', ', array_column($columns, 'Field')) . "\n";
}
} else {
echo "✗ Tabelle 'c_ai_collection_c_dokumente' existiert nicht\n";
echo "\nVerfügbare Tabellen (mit 'c_ai' oder 'c_dok' im Namen):\n";
$sql = "SHOW TABLES LIKE '%c_ai%'";
$stmt = $pdo->prepare($sql);
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
echo " - " . $row[0] . "\n";
}
$sql = "SHOW TABLES LIKE '%c_dok%'";
$stmt = $pdo->prepare($sql);
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
echo " - " . $row[0] . "\n";
}
}

View File

@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""
Test syncId über API mit Standard-Workflow
Versuche ID direkt beim Verknüpfen zu setzen und dann via Formula zu updaten
"""
import requests
import json
import subprocess
from datetime import datetime
BASE_URL = "https://crm.bitbylaw.com"
API_KEY = "e53def10eea27b92a6cd00f40a3e09a4"
HEADERS = {
"X-Api-Key": API_KEY,
"Content-Type": "application/json"
}
def api_request(method, endpoint, data=None):
url = f"{BASE_URL}/api/v1/{endpoint}"
if method == "GET":
response = requests.get(url, headers=HEADERS)
elif method == "POST":
response = requests.post(url, headers=HEADERS, json=data)
elif method == "DELETE":
response = requests.delete(url, headers=HEADERS)
elif method == "PUT":
response = requests.put(url, headers=HEADERS, json=data)
return response
def check_db(doc_id):
"""Prüfe syncId in Datenbank"""
result = subprocess.run([
"docker", "exec", "espocrm-db", "mariadb",
"-u", "espocrm", "-pdatabase_password", "espocrm",
"-e", f"SELECT sync_id FROM c_a_i_collection_c_dokumente WHERE c_dokumente_id='{doc_id}' AND deleted=0;"
], capture_output=True, text=True)
lines = result.stdout.strip().split('\n')
if len(lines) > 1:
return lines[1].strip()
return "NULL"
print("\n" + "="*80)
print(" "*20 + "Many-to-Many syncId Test")
print("="*80 + "\n")
doc_id = None
collection_id = None
test_sync_id = f"SYNC-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
try:
# 1. Entities erstellen
print("1⃣ Erstelle Entities...")
doc = api_request("POST", "CDokumente", {
"name": f"Final Test Doc {datetime.now().strftime('%H:%M:%S')}"
}).json()
doc_id = doc['id']
print(f" Doc: {doc_id}")
collection = api_request("POST", "CAICollections", {
"name": f"Final Test Col {datetime.now().strftime('%H:%M:%S')}"
}).json()
collection_id = collection['id']
print(f" Col: {collection_id}\n")
# 2. Verknüpfung erstellen
print("2⃣ Erstelle Verknüpfung...")
link_response = api_request("POST", f"CDokumente/{doc_id}/cAICollections", {
"id": collection_id
})
print(f" Status: {link_response.status_code}\n")
# 3. Direkt in DB schreiben
print("3⃣ Setze syncId direkt in Datenbank...")
db_result = subprocess.run([
"docker", "exec", "espocrm-db", "mariadb",
"-u", "espocrm", "-pdatabase_password", "espocrm",
"-e", f"UPDATE c_a_i_collection_c_dokumente SET sync_id='{test_sync_id}' WHERE c_dokumente_id='{doc_id}' AND c_a_i_collections_id='{collection_id}';"
], capture_output=True, text=True)
print(f" syncId in DB gesetzt: {test_sync_id}\n")
# 4. DB-Verifikation
print("4⃣ Verifiziere in Datenbank...")
sync_in_db = check_db(doc_id)
if sync_in_db == test_sync_id:
print(f" ✅ syncId in DB: {sync_in_db}\n")
else:
print(f" ❌ syncId falsch/NULL: {sync_in_db}\n")
# 5. Rufe über API ab
print("5⃣ Rufe Beziehung über API ab...")
relations_response = api_request("GET", f"CDokumente/{doc_id}/cAICollections")
if relations_response.status_code == 200:
relations = relations_response.json()
print(f" Status: 200\n")
if 'list' in relations and len(relations['list']) > 0:
first = relations['list'][0]
print(" Felder in Response:")
for key in sorted(first.keys()):
value = first[key]
if isinstance(value, str) and len(value) > 50:
value = value[:50] + "..."
print(f" - {key}: {value}")
print()
if 'syncId' in first and first['syncId'] == test_sync_id:
print(f" ✅ syncId in API-Response: {first['syncId']}")
result_status = "✅ VOLLSTÄNDIG ERFOLGREICH"
elif 'syncId' in first:
print(f" ⚠️ syncId in API vorhanden, aber falscher Wert: {first['syncId']}")
result_status = "⚠️ TEILWEISE ERFOLGREICH"
else:
print(f" ❌ syncId NICHT in API-Response")
result_status = "❌ API GIBT KEINE ADDITIONALCOLUMNS ZURÜCK"
else:
print(" ❌ Keine Beziehung gefunden")
result_status = "❌ FEHLGESCHLAGEN"
else:
print(f" ❌ API-Fehler: {relations_response.status_code}")
result_status = "❌ FEHLGESCHLAGEN"
finally:
print()
print("6⃣ Cleanup...")
if doc_id:
api_request("DELETE", f"CDokumente/{doc_id}")
print(f" ✓ Dokument gelöscht")
if collection_id:
api_request("DELETE", f"CAICollections/{collection_id}")
print(f" ✓ Collection gelöscht")
print("\n" + "="*80)
print(result_status)
print("="*80 + "\n")

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""
Teste verschiedene Query-Parameter um additionalColumns aus API zu bekommen
"""
import requests
import subprocess
from datetime import datetime
BASE_URL = "https://crm.bitbylaw.com"
API_KEY = "e53def10eea27b92a6cd00f40a3e09a4"
HEADERS = {
"X-Api-Key": API_KEY,
"Content-Type": "application/json"
}
doc_id = None
collection_id = None
test_sync_id = f"SYNC-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
try:
# Setup
print("Setup...")
doc = requests.post(f"{BASE_URL}/api/v1/CDokumente", headers=HEADERS, json={"name": "Test Doc"}).json()
doc_id = doc['id']
collection = requests.post(f"{BASE_URL}/api/v1/CAICollections", headers=HEADERS, json={"name": "Test Col"}).json()
collection_id = collection['id']
# Link und setze syncId in DB
requests.post(f"{BASE_URL}/api/v1/CDokumente/{doc_id}/cAICollections", headers=HEADERS, json={"id": collection_id})
subprocess.run([
"docker", "exec", "espocrm-db", "mariadb",
"-u", "espocrm", "-pdatabase_password", "espocrm",
"-e", f"UPDATE c_a_i_collection_c_dokumente SET sync_id='{test_sync_id}' WHERE c_dokumente_id='{doc_id}';"
], capture_output=True)
print(f"Doc: {doc_id}, Col: {collection_id}, syncId: {test_sync_id}\n")
# Teste verschiedene Query-Parameter
params_to_test = [
{},
{"select": "id,name,syncId"},
{"select": "id,name,sync_id"},
{"additionalColumns": "true"},
{"columns": "true"},
{"includeColumns": "true"},
{"expand": "columns"},
{"maxSize": 10, "select": "syncId"},
{"loadAdditionalFields": "true"},
]
print("="*80)
print("Teste Query-Parameter:")
print("="*80 + "\n")
for i, params in enumerate(params_to_test, 1):
param_str = ", ".join([f"{k}={v}" for k, v in params.items()]) if params else "keine"
print(f"{i}. Parameter: {param_str}")
response = requests.get(
f"{BASE_URL}/api/v1/CDokumente/{doc_id}/cAICollections",
headers=HEADERS,
params=params
)
if response.status_code == 200:
data = response.json()
if 'list' in data and len(data['list']) > 0:
first = data['list'][0]
if 'syncId' in first or 'sync_id' in first:
print(f" ✅ additionalColumn gefunden!")
print(f" syncId: {first.get('syncId', first.get('sync_id'))}")
else:
print(f" ❌ Keine additionalColumn (Felder: {len(first)})")
else:
print(f" ⚠️ Leere Liste")
else:
print(f" ❌ Status: {response.status_code}")
print()
finally:
print("Cleanup...")
if doc_id:
requests.delete(f"{BASE_URL}/api/v1/CDokumente/{doc_id}", headers=HEADERS)
if collection_id:
requests.delete(f"{BASE_URL}/api/v1/CAICollections/{collection_id}", headers=HEADERS)
print("="*80)

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""
Vereinfachter Test: Erstelle Dokument und CAICollection über API und teste Verknüpfung
"""
import requests
import json
import sys
from datetime import datetime
BASE_URL = "https://crm.bitbylaw.com"
API_KEY = "e53def10eea27b92a6cd00f40a3e09a4" # User: marvin
HEADERS = {
"X-Api-Key": API_KEY,
"Content-Type": "application/json"
}
def api_request(method, endpoint, data=None):
"""API-Request"""
url = f"{BASE_URL}/api/v1/{endpoint}"
try:
if method == "GET":
response = requests.get(url, headers=HEADERS)
elif method == "POST":
response = requests.post(url, headers=HEADERS, json=data)
elif method == "DELETE":
response = requests.delete(url, headers=HEADERS)
print(f" {method} {endpoint} -> Status: {response.status_code}")
if response.status_code >= 400:
print(f" Error Response: {response.text[:200]}")
return None
return response.json() if response.text else {}
except Exception as e:
print(f" Exception: {e}")
return None
print("\n" + "="*80)
print(" "*25 + "Junction API Test - Simplified")
print("="*80 + "\n")
doc_id = None
collection_id = None
test_sync_id = f"SYNC-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
try:
# 1. CDokumente erstellen
print("1⃣ Erstelle CDokumente...")
doc = api_request("POST", "CDokumente", {
"name": f"API-Test Dokument {datetime.now().strftime('%H:%M:%S')}",
"description": "Für Junction-Test"
})
if doc and 'id' in doc:
doc_id = doc['id']
print(f" ✓ Dokument erstellt: {doc_id}\n")
else:
print(" ❌ Fehler\n")
sys.exit(1)
# 2. Versuche CAICollection zu erstellen (könnte fehlschlagen wegen Berechtigungen)
print("2⃣ Versuche CAICollection zu erstellen...")
collection = api_request("POST", "CAICollections", {
"name": f"API-Test Collection {datetime.now().strftime('%H:%M:%S')}",
"description": "Für Junction-Test"
})
if collection and 'id' in collection:
collection_id = collection['id']
print(f" ✓ Collection erstellt: {collection_id}\n")
# 3. Verknüpfen mit syncId
print("3⃣ Verknüpfe mit syncId...")
print(f" syncId: {test_sync_id}")
join_result = api_request("POST", f"CDokumente/{doc_id}/cAICollections", {
"id": collection_id,
"columns": {
"syncId": test_sync_id
}
})
print(f" Join Result: {join_result}\n")
# 4. Abrufen
print("4⃣ Rufe Beziehung ab...")
relations = api_request("GET", f"CDokumente/{doc_id}/cAICollections")
if relations:
print(f"\nAPI Response:")
print(json.dumps(relations, indent=2, ensure_ascii=False))
print()
if 'list' in relations and len(relations['list']) > 0:
first = relations['list'][0]
if 'syncId' in first:
print(f"✅ syncId gefunden in API: {first['syncId']}")
if first['syncId'] == test_sync_id:
print(f"✅ syncId-Wert stimmt überein!")
else:
print(f"⚠️ Wert stimmt nicht: {first['syncId']} != {test_sync_id}")
else:
print(f"❌ syncId NICHT in Response (Felder: {list(first.keys())})")
else:
print("❌ Keine Relation gefunden")
else:
print(" ❌ Keine Berechtigung für CAICollections\n")
print(" Das ist ein Berechtigungsproblem, kein Problem mit den additionalColumns\n")
finally:
print("\n5⃣ Cleanup...")
if doc_id:
api_request("DELETE", f"CDokumente/{doc_id}")
print(f" ✓ Dokument gelöscht")
if collection_id:
api_request("DELETE", f"CAICollections/{collection_id}")
print(f" ✓ Collection gelöscht")
print("\n" + "="*80 + "\n")

View File

@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""
Teste verschiedene API-Syntaxen für additionalColumns
"""
import requests
import json
from datetime import datetime
BASE_URL = "https://crm.bitbylaw.com"
API_KEY = "e53def10eea27b92a6cd00f40a3e09a4"
HEADERS = {
"X-Api-Key": API_KEY,
"Content-Type": "application/json"
}
def api_request(method, endpoint, data=None):
url = f"{BASE_URL}/api/v1/{endpoint}"
try:
if method == "GET":
response = requests.get(url, headers=HEADERS)
elif method == "POST":
response = requests.post(url, headers=HEADERS, json=data)
elif method == "DELETE":
response = requests.delete(url, headers=HEADERS)
return response
except Exception as e:
print(f" Exception: {e}")
return None
print("\n" + "="*80)
print(" "*20 + "API Syntax Variations Test")
print("="*80 + "\n")
doc_id = None
collection_id = None
try:
# Erstelle Test-Entities
print("Erstelle Test-Entities...")
doc = api_request("POST", "CDokumente", {
"name": f"Test Doc {datetime.now().strftime('%H:%M:%S')}"
}).json()
doc_id = doc['id']
print(f" Doc: {doc_id}")
collection = api_request("POST", "CAICollections", {
"name": f"Test Col {datetime.now().strftime('%H:%M:%S')}"
}).json()
collection_id = collection['id']
print(f" Col: {collection_id}\n")
# Teste verschiedene Syntaxen
variations = [
{
"name": "Variante 1: columns mit syncId",
"data": {
"id": collection_id,
"columns": {
"syncId": "TEST-SYNC-001"
}
}
},
{
"name": "Variante 2: Direkt syncId im Body",
"data": {
"id": collection_id,
"syncId": "TEST-SYNC-002"
}
},
{
"name": "Variante 3: columns mit sync_id (Snake-Case)",
"data": {
"id": collection_id,
"columns": {
"sync_id": "TEST-SYNC-003"
}
}
},
{
"name": "Variante 4: additionalColumns",
"data": {
"id": collection_id,
"additionalColumns": {
"syncId": "TEST-SYNC-004"
}
}
}
]
for i, variant in enumerate(variations, 1):
print(f"{i}. {variant['name']}")
# Lösche vorherige Verknüpfung
api_request("DELETE", f"CDokumente/{doc_id}/cAICollections", {"id": collection_id})
# Verknüpfe mit Variante
response = api_request("POST", f"CDokumente/{doc_id}/cAICollections", variant['data'])
print(f" Status: {response.status_code}")
if response.status_code == 200:
# Prüfe Datenbank
import subprocess
db_check = subprocess.run([
"docker", "exec", "espocrm-db", "mariadb",
"-u", "espocrm", "-pdatabase_password", "espocrm",
"-e", f"SELECT sync_id FROM c_a_i_collection_c_dokumente WHERE c_dokumente_id='{doc_id}' AND deleted=0;"
], capture_output=True, text=True)
lines = db_check.stdout.strip().split('\n')
if len(lines) > 1:
sync_id_value = lines[1].strip()
if sync_id_value and sync_id_value != "NULL":
print(f" ✅ syncId in DB: {sync_id_value}")
else:
print(f" ❌ syncId ist NULL")
else:
print(f" ⚠️ Keine Zeile in DB")
else:
print(f" ❌ Request fehlgeschlagen: {response.text[:100]}")
print()
finally:
print("Cleanup...")
if doc_id:
api_request("DELETE", f"CDokumente/{doc_id}")
if collection_id:
api_request("DELETE", f"CAICollections/{collection_id}")
print("="*80 + "\n")

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""
Test-Skript zum Überprüfen der Many-to-Many-Beziehung mit additionalColumns
zwischen CDokumente und CAICollections
"""
import requests
import json
import sys
from datetime import datetime
# API-Konfiguration (aus e2e_tests.py übernommen)
BASE_URL = "https://crm.bitbylaw.com"
API_KEY = "2b0747ca34d15032aa233ae043cc61bc"
HEADERS = {
"X-Api-Key": API_KEY,
"Content-Type": "application/json"
}
def api_request(method, endpoint, data=None):
"""Führt einen API-Request aus"""
url = f"{BASE_URL}/api/v1/{endpoint}"
try:
if method == "GET":
response = requests.get(url, headers=HEADERS)
elif method == "POST":
response = requests.post(url, headers=HEADERS, json=data)
elif method == "PUT":
response = requests.put(url, headers=HEADERS, json=data)
elif method == "DELETE":
response = requests.delete(url, headers=HEADERS)
else:
raise ValueError(f"Unknown method: {method}")
response.raise_for_status()
return response.json() if response.text else {}
except requests.exceptions.RequestException as e:
print(f"❌ API Error: {e}")
if hasattr(e.response, 'text'):
print(f" Response: {e.response.text}")
return None
def main():
print("="*80)
print(" "*20 + "Many-to-Many Junction Test")
print("="*80)
print()
# Test-Daten
test_sync_id = f"TEST-SYNC-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
doc_id = None
collection_id = None
try:
# 1. CDokumente-Eintrag erstellen
print("1⃣ Erstelle Test-Dokument...")
doc_data = {
"name": f"Test-Dokument für Junction-Test {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"description": "Test-Dokument für Many-to-Many mit syncId"
}
doc_result = api_request("POST", "CDokumente", doc_data)
if not doc_result or 'id' not in doc_result:
print("❌ Fehler beim Erstellen des Dokuments")
return False
doc_id = doc_result['id']
print(f"✓ Dokument erstellt: {doc_id}")
print()
# 2. CAICollections-Eintrag erstellen
print("2⃣ Erstelle Test-Collection...")
collection_data = {
"name": f"Test-Collection für Junction-Test {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"description": "Test-Collection für Many-to-Many mit syncId"
}
collection_result = api_request("POST", "CAICollections", collection_data)
if not collection_result or 'id' not in collection_result:
print("❌ Fehler beim Erstellen der Collection")
return False
collection_id = collection_result['id']
print(f"✓ Collection erstellt: {collection_id}")
print()
# 3. Verknüpfung erstellen mit syncId
print("3⃣ Verknüpfe Dokument mit Collection (mit syncId)...")
print(f" syncId: {test_sync_id}")
link_data = {
"id": collection_id,
"columns": {
"syncId": test_sync_id
}
}
link_result = api_request("POST", f"CDokumente/{doc_id}/cAICollections", link_data)
if link_result is None:
print("❌ Fehler beim Verknüpfen")
return False
print(f"✓ Verknüpfung erstellt")
print()
# 4. Beziehung über API abrufen
print("4⃣ Rufe Beziehung über API ab...")
relations = api_request("GET", f"CDokumente/{doc_id}/cAICollections")
if not relations:
print("❌ Fehler beim Abrufen der Beziehung")
return False
print(f"✓ Beziehung abgerufen")
print("\nAPI-Response:")
print("-" * 80)
print(json.dumps(relations, indent=2, ensure_ascii=False))
print("-" * 80)
print()
# 5. Prüfe ob syncId vorhanden ist
print("5⃣ Prüfe ob syncId in der Response vorhanden ist...")
if 'list' in relations and len(relations['list']) > 0:
first_relation = relations['list'][0]
if 'syncId' in first_relation:
returned_sync_id = first_relation['syncId']
if returned_sync_id == test_sync_id:
print(f"✅ syncId korrekt zurückgegeben: {returned_sync_id}")
success = True
else:
print(f"⚠️ syncId zurückgegeben, aber Wert stimmt nicht überein:")
print(f" Erwartet: {test_sync_id}")
print(f" Erhalten: {returned_sync_id}")
success = False
else:
print("❌ syncId ist NICHT in der API-Response vorhanden")
print(f" Vorhandene Felder: {list(first_relation.keys())}")
success = False
else:
print("❌ Keine Beziehungen in der Response gefunden")
success = False
print()
# 6. Direkter Datenbankcheck (optional)
print("6⃣ Prüfe Datenbank direkt...")
import subprocess
db_check = subprocess.run([
"docker", "exec", "espocrm-db", "mariadb",
"-u", "espocrm", "-pdatabase_password", "espocrm",
"-e", f"SELECT * FROM c_a_i_collection_c_dokumente WHERE c_dokumente_id='{doc_id}' AND deleted=0;"
], capture_output=True, text=True)
if db_check.returncode == 0:
print("Datenbank-Inhalt:")
print("-" * 80)
print(db_check.stdout)
print("-" * 80)
else:
print(f"⚠️ Konnte Datenbank nicht direkt abfragen: {db_check.stderr}")
print()
return success
finally:
# Cleanup
print("7⃣ Räume Test-Daten auf...")
if doc_id:
result = api_request("DELETE", f"CDokumente/{doc_id}")
if result is not None:
print(f"✓ Dokument gelöscht: {doc_id}")
if collection_id:
result = api_request("DELETE", f"CAICollections/{collection_id}")
if result is not None:
print(f"✓ Collection gelöscht: {collection_id}")
print()
if __name__ == "__main__":
print()
result = main()
print("="*80)
if result:
print("✅ TEST ERFOLGREICH - Many-to-Many mit additionalColumns funktioniert!")
else:
print("❌ TEST FEHLGESCHLAGEN - syncId nicht in API-Response")
print("="*80)
sys.exit(0 if result else 1)

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""
Finaler Test: Junction-Entity API mit Filterung und syncId
"""
import requests
import json
BASE_URL = "https://crm.bitbylaw.com"
API_KEY = "e53def10eea27b92a6cd00f40a3e09a4"
HEADERS = {
"X-Api-Key": API_KEY,
"Content-Type": "application/json"
}
print("\n" + "="*80)
print(" "*25 + "Junction-Entity API - SUCCESS!")
print("="*80 + "\n")
# Test 1: Alle Junction-Einträge
print("1⃣ Alle Verknüpfungen abrufen:")
response = requests.get(
f"{BASE_URL}/api/v1/CAICollectionCDokumente",
headers=HEADERS,
params={"maxSize": 10}
)
print(f" Status: {response.status_code}")
data = response.json()
print(f" Total: {data['total']}")
print(f" ✅ API funktioniert!\n")
# Test 2: Filterung nach Dokument-ID
print("2⃣ Filterung nach Dokument-ID (testdoc999):")
response = requests.get(
f"{BASE_URL}/api/v1/CAICollectionCDokumente",
headers=HEADERS,
params={
"where[0][type]": "equals",
"where[0][attribute]": "cDokumenteId",
"where[0][value]": "testdoc999",
"select": "id,cDokumenteId,cAICollectionsId,syncId"
}
)
print(f" Status: {response.status_code}")
if response.status_code == 200:
data = response.json()
print(json.dumps(data, indent=2, ensure_ascii=False))
if data['list'] and 'syncId' in data['list'][0]:
print(f"\n ✅ syncId gefunden: {data['list'][0]['syncId']}")
print()
# Test 3: Suche nach syncId
print("3⃣ Filterung nach syncId (SYNC-TEST-999):")
response = requests.get(
f"{BASE_URL}/api/v1/CAICollectionCDokumente",
headers=HEADERS,
params={
"where[0][type]": "equals",
"where[0][attribute]": "syncId",
"where[0][value]": "SYNC-TEST-999"
}
)
print(f" Status: {response.status_code}")
if response.status_code == 200:
data = response.json()
print(json.dumps(data, indent=2, ensure_ascii=False))
if data['list']:
entry = data['list'][0]
print(f"\n ✅ Verknüpfung gefunden:")
print(f" Dokument: {entry['cDokumenteId']}")
print(f" Collection: {entry['cAICollectionsId']}")
print(f" Sync-ID: {entry['syncId']}")
print()
print("="*80)
print("✅ VOLLSTÄNDIGER ERFOLG!")
print("="*80)
print("\nDie Junction-Entity ist via REST-API verfügbar und gibt die syncId zurück!")
print("- Endpoint: /api/v1/CAICollectionCDokumente")
print("- Filterung funktioniert (where-Clauses)")
print("- Alle additionalColumns (syncId) sind in der Response")
print("\n" + "="*80 + "\n")

View File

@@ -0,0 +1,190 @@
<?php
/**
* Test für Many-to-Many-Beziehung mit additionalColumns
* Testet die Beziehung zwischen CDokumente und CAICollections mit syncId
*/
require_once 'bootstrap.php';
$app = new \Espo\Core\Application();
$container = $app->getContainer();
$entityManager = $container->getByClass(\Espo\ORM\EntityManager::class);
$recordServiceContainer = $container->getByClass(\Espo\Core\Record\ServiceContainer::class);
echo "\n" . str_repeat("=", 80) . "\n";
echo str_repeat(" ", 20) . "Many-to-Many Junction Test (Internal API)\n";
echo str_repeat("=", 80) . "\n\n";
$docId = null;
$collectionId = null;
$testSyncId = "TEST-SYNC-" . date('Ymd-His');
try {
// 1. CDokumente erstellen (ohne das required dokument-Feld - nur für Test)
echo "1⃣ Erstelle Test-Dokument...\n";
// Direktes ORM-Entity erstellen (umgeht Validierung)
$doc = $entityManager->createEntity('CDokumente', [
'name' => 'Test-Dokument für Junction-Test ' . date('Y-m-d H:i:s'),
'description' => 'Test-Dokument für Many-to-Many mit syncId',
]);
if (!$doc) {
throw new Exception("Fehler beim Erstellen des Dokuments");
}
$docId = $doc->getId();
echo "✓ Dokument erstellt: {$docId}\n\n";
// 2. CAICollections erstellen
echo "2⃣ Erstelle Test-Collection...\n";
$collection = $entityManager->createEntity('CAICollections', [
'name' => 'Test-Collection für Junction-Test ' . date('Y-m-d H:i:s'),
'description' => 'Test-Collection für Many-to-Many mit syncId',
]);
if (!$collection) {
throw new Exception("Fehler beim Erstellen der Collection");
}
$collectionId = $collection->getId();
echo "✓ Collection erstellt: {$collectionId}\n\n";
// 3. Beziehung erstellen mit syncId
echo "3⃣ Verknüpfe Dokument mit Collection (mit syncId)...\n";
echo " syncId: {$testSyncId}\n";
$repository = $entityManager->getRDBRepository('CDokumente');
$relation = $repository->getRelation($doc, 'cAICollections');
// Verbinde mit additionalColumns
$relation->relateById($collectionId, [
'syncId' => $testSyncId
]);
echo "✓ Verknüpfung erstellt\n\n";
// 4. Beziehung wieder laden und prüfen
echo "4⃣ Lade Beziehung und prüfe syncId...\n";
// Reload Dokument
$doc = $entityManager->getEntity('CDokumente', $docId);
// Lade die verknüpften Collections
$repository = $entityManager->getRDBRepository('CDokumente');
$relation = $repository->getRelation($doc, 'cAICollections');
// Hole alle verknüpften Collections mit Columns
$collections = $relation->find();
echo " Anzahl verknüpfter Collections: " . count($collections) . "\n";
$found = false;
foreach ($collections as $col) {
if ($col->getId() === $collectionId) {
echo " Collection gefunden: {$col->getId()}\n";
// Hole die Middle-Columns
$relationData = $relation->getColumnAttributes($col, ['syncId']);
echo "\n Relation-Daten:\n";
echo " " . str_repeat("-", 76) . "\n";
echo " " . json_encode($relationData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
echo " " . str_repeat("-", 76) . "\n\n";
if (isset($relationData['syncId'])) {
$returnedSyncId = $relationData['syncId'];
if ($returnedSyncId === $testSyncId) {
echo " ✅ syncId korrekt geladen: {$returnedSyncId}\n";
$found = true;
} else {
echo " ⚠️ syncId geladen, aber Wert stimmt nicht:\n";
echo " Erwartet: {$testSyncId}\n";
echo " Erhalten: {$returnedSyncId}\n";
}
} else {
echo " ❌ syncId ist NICHT in den Relation-Daten vorhanden\n";
echo " Verfügbare Felder: " . implode(', ', array_keys($relationData)) . "\n";
}
}
}
if (!$found && count($collections) > 0) {
echo " ⚠️ Collection verknüpft, aber syncId-Prüfung fehlgeschlagen\n";
} elseif (!$found) {
echo " ❌ Keine verknüpfte Collection gefunden\n";
}
echo "\n";
// 5. Direkter Datenbank-Check
echo "5⃣ Prüfe Datenbank direkt...\n";
$pdo = $entityManager->getPDO();
$stmt = $pdo->prepare(
"SELECT * FROM c_a_i_collection_c_dokumente
WHERE c_dokumente_id = :docId AND deleted = 0"
);
$stmt->execute(['docId' => $docId]);
$dbRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo " Datenbank-Zeilen gefunden: " . count($dbRows) . "\n";
if (count($dbRows) > 0) {
echo "\n Datenbank-Inhalt:\n";
echo " " . str_repeat("-", 76) . "\n";
foreach ($dbRows as $row) {
echo " " . json_encode($row, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
}
echo " " . str_repeat("-", 76) . "\n";
foreach ($dbRows as $row) {
if ($row['c_a_i_collections_id'] === $collectionId) {
if (isset($row['sync_id']) && $row['sync_id'] === $testSyncId) {
echo " ✅ syncId in Datenbank korrekt: {$row['sync_id']}\n";
} else {
echo " ❌ syncId in Datenbank falsch oder fehlend\n";
}
}
}
}
echo "\n";
$success = $found;
} catch (Exception $e) {
echo "\n❌ Fehler: " . $e->getMessage() . "\n";
echo " Stack Trace:\n";
echo " " . $e->getTraceAsString() . "\n\n";
$success = false;
} finally {
// Cleanup
echo "6⃣ Räume Test-Daten auf...\n";
if ($docId) {
try {
$entityManager->removeEntity($entityManager->getEntity('CDokumente', $docId));
echo "✓ Dokument gelöscht: {$docId}\n";
} catch (Exception $e) {
echo "⚠️ Fehler beim Löschen des Dokuments: {$e->getMessage()}\n";
}
}
if ($collectionId) {
try {
$entityManager->removeEntity($entityManager->getEntity('CAICollections', $collectionId));
echo "✓ Collection gelöscht: {$collectionId}\n";
} catch (Exception $e) {
echo "⚠️ Fehler beim Löschen der Collection: {$e->getMessage()}\n";
}
}
echo "\n";
}
echo str_repeat("=", 80) . "\n";
if ($success) {
echo "✅ TEST ERFOLGREICH - Many-to-Many mit additionalColumns funktioniert!\n";
} else {
echo "❌ TEST FEHLGESCHLAGEN - syncId nicht korrekt verfügbar\n";
}
echo str_repeat("=", 80) . "\n\n";
exit($success ? 0 : 1);

View File

@@ -739,6 +739,81 @@ class EntityValidator:
print_success(f"Alle {len(php_files)} PHP-Dateien sind syntaktisch korrekt") print_success(f"Alle {len(php_files)} PHP-Dateien sind syntaktisch korrekt")
return True return True
def show_error_logs(self):
"""Zeige die letzten Fehlerlog-Einträge aus data/logs/."""
from datetime import datetime
print_header("FEHLERLOG ANALYSE")
logs_path = self.base_path / "data" / "logs"
if not logs_path.exists():
print_warning("Logs-Verzeichnis nicht gefunden")
return
# Finde das neueste Log-File
today = datetime.now().strftime("%Y-%m-%d")
log_file = logs_path / f"espo-{today}.log"
if not log_file.exists():
# Fallback: Finde das neueste Log-File
log_files = sorted(logs_path.glob("espo-*.log"), key=lambda f: f.stat().st_mtime, reverse=True)
if log_files:
log_file = log_files[0]
print_info(f"Kein Log für heute gefunden, verwende: {log_file.name}")
else:
print_warning("Keine Log-Dateien gefunden")
return
print_info(f"Analysiere: {log_file.name}")
try:
with open(log_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
if not lines:
print_info("Log-Datei ist leer")
return
# Zeige die letzten 50 Zeilen
last_lines = lines[-50:]
# Filter für Fehler und Warnungen
errors = []
warnings = []
for line in last_lines:
line_upper = line.upper()
if 'ERROR' in line_upper or 'FATAL' in line_upper or 'EXCEPTION' in line_upper:
errors.append(line.strip())
elif 'WARNING' in line_upper or 'WARN' in line_upper:
warnings.append(line.strip())
if errors:
print_error(f"\n{len(errors)} Fehler in den letzten 50 Log-Zeilen gefunden:\n")
for i, error in enumerate(errors[-10:], 1): # Zeige max. 10 Fehler
print(f"{Colors.RED}{i}.{Colors.END} {error}")
if len(errors) > 10:
print(f"\n{Colors.YELLOW}... und {len(errors) - 10} weitere Fehler{Colors.END}")
if warnings:
print_warning(f"\n{len(warnings)} Warnungen in den letzten 50 Log-Zeilen gefunden:\n")
for i, warning in enumerate(warnings[-5:], 1): # Zeige max. 5 Warnungen
print(f"{Colors.YELLOW}{i}.{Colors.END} {warning}")
if len(warnings) > 5:
print(f"\n{Colors.YELLOW}... und {len(warnings) - 5} weitere Warnungen{Colors.END}")
if not errors and not warnings:
print_info("Keine Fehler oder Warnungen in den letzten 50 Log-Zeilen gefunden")
print_info("\nLetzte 10 Log-Zeilen:")
for line in last_lines[-10:]:
print(f" {line.strip()}")
print(f"\n{Colors.BLUE}{Colors.END} Vollständige Log-Datei: {log_file}")
print(f"{Colors.BLUE}{Colors.END} Zum Anzeigen: tail -50 {log_file}")
except Exception as e:
print_error(f"Fehler beim Lesen der Log-Datei: {e}")
def run_rebuild(self) -> bool: def run_rebuild(self) -> bool:
"""Führe den EspoCRM Rebuild aus.""" """Führe den EspoCRM Rebuild aus."""
print_header("10. ESPOCRM REBUILD") print_header("10. ESPOCRM REBUILD")
@@ -811,6 +886,9 @@ class EntityValidator:
print_error("Rebuild fehlgeschlagen:") print_error("Rebuild fehlgeschlagen:")
if result.stderr: if result.stderr:
print(f"\n{result.stderr}") print(f"\n{result.stderr}")
# Zeige automatisch die letzten Fehlerlog-Einträge an
self.show_error_logs()
return False return False
else: else:
print_warning("Kein EspoCRM Docker-Container gefunden") print_warning("Kein EspoCRM Docker-Container gefunden")
@@ -862,6 +940,9 @@ class EntityValidator:
print_error("Rebuild fehlgeschlagen:") print_error("Rebuild fehlgeschlagen:")
if result.stderr: if result.stderr:
print(f"\n{result.stderr}") print(f"\n{result.stderr}")
# Zeige automatisch die letzten Fehlerlog-Einträge an
self.show_error_logs()
return False return False
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
print_error("Rebuild-Timeout (>60 Sekunden)") print_error("Rebuild-Timeout (>60 Sekunden)")

View File

@@ -1,72 +1,39 @@
# Workflow Definitions # Workflow Documentation
This directory contains workflow definitions in JSON format that can be imported into EspoCRM using the workflow manager script. Dokumentation für EspoCRM Workflow-Management.
## File Format ## Workflow-Format & Management
### Simple Workflow Siehe: `custom/docs/workflows/README.md` für vollständige Workflow-Dokumentation inkl.:
```json - Simple Workflow JSON-Format
{ - BPM Flowchart Format
"type": "simple", - Trigger Types (afterRecordSaved, afterRecordCreated, scheduled, manual)
"name": "workflow-name", - Action Types (sendEmail, createEntity, updateEntity, etc.)
"entity_type": "EntityName", - Condition Types
"trigger_type": "afterRecordSaved", - Import/Export mit workflow_manager.php
"is_active": true,
"description": "Description of what this workflow does",
"category": "Category Name",
"conditions_all": [],
"conditions_any": [],
"conditions_formula": null,
"actions": []
}
```
**Trigger Types:** ## Workflow-Befehle
- `afterRecordSaved` - After record is created or updated
- `afterRecordCreated` - Only after record is created
- `scheduled` - Runs on a schedule
- `manual` - Manually triggered
**Condition Types:**
- `equals`, `notEquals`, `greaterThan`, `lessThan`, `contains`, `notContains`, `isEmpty`, `isNotEmpty`, `isTrue`, `isFalse`, `wasEqual`, `wasNotEqual`, `changed`, `notChanged`
**Action Types:**
- `sendEmail` - Send email to recipient
- `createEntity` - Create a new record
- `updateEntity` - Update current record
- `relateTo` - Link to another record
- `unrelateFrom` - Unlink from record
- `applyAssignmentRule` - Apply assignment rules
- `createNotification` - Create notification
### BPM Flowchart
```json
{
"type": "bpm",
"name": "flowchart-name",
"target_type": "EntityName",
"is_active": true,
"description": "Description",
"data": {
"list": []
},
"elements_data_hash": {},
"event_start_all_id_list": []
}
```
## Usage
Import a workflow:
```bash ```bash
docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php import /var/www/html/custom/workflows/your-workflow.json # Alle Workflows auflisten
php custom/scripts/workflow_manager.php list
# Workflow importieren
php custom/scripts/workflow_manager.php import custom/workflows/my-workflow.json
# Alle Workflows exportieren
php custom/scripts/workflow_manager.php export
# Workflow löschen
php custom/scripts/workflow_manager.php delete workflow-name
``` ```
Export a workflow: ## Workflow-Dateien
```bash
docker exec espocrm php /var/www/html/custom/scripts/workflow_manager.php export <workflow-id> /var/www/html/custom/workflows/exported.json
```
## Examples Workflow-Definitionen werden als JSON-Dateien in diesem Verzeichnis gespeichert und können mit dem workflow_manager.php Tool importiert werden.
- `vmh-erstberatung-abschliessen.json` - Sends email and sets status when consultation is completed **Datei-Naming:** `{workflow-name}.json` (Kleinbuchstaben, Bindestriche)
---
Für Details siehe: **custom/docs/workflows/README.md**

View File

@@ -358,7 +358,7 @@ return [
0 => 'youtube.com', 0 => 'youtube.com',
1 => 'google.com' 1 => 'google.com'
], ],
'microtime' => 1773002102.630821, 'microtime' => 1773092106.892439,
'siteUrl' => 'https://crm.bitbylaw.com', 'siteUrl' => 'https://crm.bitbylaw.com',
'fullTextSearchMinLength' => 4, 'fullTextSearchMinLength' => 4,
'webSocketUrl' => 'ws://api.bitbylaw.com:5000/espocrm/ws', 'webSocketUrl' => 'ws://api.bitbylaw.com:5000/espocrm/ws',

View File

@@ -1,7 +1,7 @@
<?php <?php
return [ return [
'cacheTimestamp' => 1773002102, 'cacheTimestamp' => 1773092107,
'microtimeState' => 1773002102.752942, 'microtimeState' => 1773092107.012107,
'currencyRates' => [ 'currencyRates' => [
'EUR' => 1.0 'EUR' => 1.0
], ],