Compare commits
4 Commits
53dd8f33d4
...
63e3841f86
| Author | SHA1 | Date | |
|---|---|---|---|
| 63e3841f86 | |||
| 2e9db78c6e | |||
| 3361cffb14 | |||
| 47634c81ef |
548
.github/agents/espocrm-developer.agent.md
vendored
Normal file
548
.github/agents/espocrm-developer.agent.md
vendored
Normal 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.
|
||||||
313
.github/agents/espocrm-docs-maintainer.agent.md
vendored
Normal file
313
.github/agents/espocrm-docs-maintainer.agent.md
vendored
Normal 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.
|
||||||
361
custom/DOCUMENTATION_INDEX.md
Normal file
361
custom/DOCUMENTATION_INDEX.md
Normal 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`
|
||||||
10
custom/Espo/Custom/Controllers/CAICollectionCDokumente.php
Normal file
10
custom/Espo/Custom/Controllers/CAICollectionCDokumente.php
Normal 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
|
||||||
|
}
|
||||||
7
custom/Espo/Custom/Controllers/CAICollections.php
Normal file
7
custom/Espo/Custom/Controllers/CAICollections.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Espo\Custom\Controllers;
|
||||||
|
|
||||||
|
class CAICollections extends \Espo\Core\Templates\Controllers\BasePlus
|
||||||
|
{
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/ar_AR/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/ar_AR/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "الاجتماعات",
|
||||||
|
"calls": "المكالمات",
|
||||||
|
"tasks": "مهام"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "إنشاء {الكيانTypeTranslated}"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/bg_BG/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/bg_BG/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Срещи",
|
||||||
|
"calls": "Разговори",
|
||||||
|
"tasks": "Задачи"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Създаване на AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/cs_CZ/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/cs_CZ/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Schůzky",
|
||||||
|
"calls": "Hovory",
|
||||||
|
"tasks": "Úkoly"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Vytvořit AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/da_DK/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/da_DK/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Møder",
|
||||||
|
"calls": "Opkald",
|
||||||
|
"tasks": "Opgaver"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Opret AI-Collections "
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"calls": "Anrufe",
|
||||||
|
"tasks": "Aufgaben"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "AI-Collections erstellen"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
10
custom/Espo/Custom/Resources/i18n/el_GR/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/el_GR/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Συναντήσεις",
|
||||||
|
"calls": "Κλήσεις",
|
||||||
|
"tasks": "Εργασίες"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Δημιουργία AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -1 +1,8 @@
|
|||||||
{}
|
{
|
||||||
|
"fields": {
|
||||||
|
"cAICollections": "AI Collections"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"cAICollections": "AI Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
custom/Espo/Custom/Resources/i18n/en_US/CAICollections.json
Normal file
12
custom/Espo/Custom/Resources/i18n/en_US/CAICollections.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"meetings": "Meetings",
|
||||||
|
"calls": "Calls",
|
||||||
|
"tasks": "Tasks"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Create AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/es_ES/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/es_ES/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Reuniones",
|
||||||
|
"calls": "Llamadas",
|
||||||
|
"tasks": "Tareas"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Crear AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/es_MX/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/es_MX/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Presentaciones",
|
||||||
|
"calls": "Llamadas",
|
||||||
|
"tasks": "Tareas"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Crear AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/fa_IR/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/fa_IR/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "جلسات",
|
||||||
|
"calls": "تماس ها",
|
||||||
|
"tasks": "وظایف"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "ایجاد AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Créer un AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/hr_HR/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/hr_HR/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Sastanci",
|
||||||
|
"calls": "Pozivi",
|
||||||
|
"tasks": "Zadaci"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Kreiraj AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/hu_HU/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/hu_HU/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "találkozók",
|
||||||
|
"calls": "felhívja",
|
||||||
|
"tasks": "Feladatok"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "{EntityTypeTranslated} létrehozása"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Buat AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/it_IT/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/it_IT/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Riunioni",
|
||||||
|
"calls": "Chiamate",
|
||||||
|
"tasks": "Compiti"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Crea AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/ja_JP/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/ja_JP/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "会議",
|
||||||
|
"calls": "通話",
|
||||||
|
"tasks": "タスク"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "AI-Collections を作成する"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/lt_LT/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/lt_LT/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Susitikimai",
|
||||||
|
"calls": "Skambučiai",
|
||||||
|
"tasks": "Užduotys"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Sukurti AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/lv_LV/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/lv_LV/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Tikšanās",
|
||||||
|
"calls": "Zvani",
|
||||||
|
"tasks": "Uzdevumi"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Izveidot AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/nb_NO/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/nb_NO/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Møter",
|
||||||
|
"calls": "Samtaler",
|
||||||
|
"tasks": "Oppgaver"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Opprett AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/nl_NL/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/nl_NL/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Vergaderingen",
|
||||||
|
"calls": "Gesprekken",
|
||||||
|
"tasks": "Taken"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Creëer AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/pl_PL/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/pl_PL/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Spotkania",
|
||||||
|
"calls": "Połączenia",
|
||||||
|
"tasks": "Zadania"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Utwórz AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/pt_BR/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/pt_BR/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Reuniões",
|
||||||
|
"calls": "Ligações",
|
||||||
|
"tasks": "Tarefas"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Criar AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/pt_PT/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/pt_PT/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Reuniões",
|
||||||
|
"calls": "Chamadas",
|
||||||
|
"tasks": "Tarefas"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Criar AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Creare AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/ru_RU/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/ru_RU/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Встречи",
|
||||||
|
"calls": "Звонки",
|
||||||
|
"tasks": "Задачи"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Создать AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/sk_SK/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/sk_SK/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Stretnutia",
|
||||||
|
"calls": "Hovory",
|
||||||
|
"tasks": "Úlohy"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Vytvoriť AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/sl_SI/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/sl_SI/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Srečanja",
|
||||||
|
"calls": "Klici",
|
||||||
|
"tasks": "Naloge"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Ustvari AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/sr_RS/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/sr_RS/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Ročišta",
|
||||||
|
"calls": "Pozivi",
|
||||||
|
"tasks": "Zadaci"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Napravi AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/sv_SE/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/sv_SE/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Möten",
|
||||||
|
"calls": "Samtal",
|
||||||
|
"tasks": "Uppgifter"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Skapa AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/th_TH/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/th_TH/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "การประชุม",
|
||||||
|
"calls": "โทร",
|
||||||
|
"tasks": "งาน"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "สร้าง AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/tr_TR/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/tr_TR/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Toplantılar",
|
||||||
|
"calls": "(Seslenme)",
|
||||||
|
"tasks": "Görevler"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "AI-Collections oluştur"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/uk_UA/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/uk_UA/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Зустрічі",
|
||||||
|
"calls": "Дзвінки",
|
||||||
|
"tasks": "Завдання"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "Створити AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/ur_IN/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/ur_IN/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "ملاقاتیں",
|
||||||
|
"calls": "کالز",
|
||||||
|
"tasks": "کام"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "{entityTypetranslated} بنائیں"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/vi_VN/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/vi_VN/CAICollections.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/zh_CN/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/zh_CN/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "会议",
|
||||||
|
"calls": "通话",
|
||||||
|
"tasks": "任务"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "创建 AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/zh_TW/CAICollections.json
Normal file
10
custom/Espo/Custom/Resources/i18n/zh_TW/CAICollections.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "會議",
|
||||||
|
"calls": "通話",
|
||||||
|
"tasks": "任務"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAICollections": "建立AI-Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "syncId"
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "cAICollections",
|
||||||
|
"fullWidth": true
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "cDokumente",
|
||||||
|
"fullWidth": true
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "syncId",
|
||||||
|
"fullWidth": true
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -47,6 +47,6 @@
|
|||||||
},
|
},
|
||||||
"stream": {
|
"stream": {
|
||||||
"sticked": false,
|
"sticked": false,
|
||||||
"index": 12
|
"index": 11
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"link": true,
|
||||||
|
"width": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "createdAt",
|
||||||
|
"width": 30
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,10 @@
|
|||||||
"mietobjekt2dokumente": {
|
"mietobjekt2dokumente": {
|
||||||
"layout": null,
|
"layout": null,
|
||||||
"selectPrimaryFilterName": null
|
"selectPrimaryFilterName": null
|
||||||
|
},
|
||||||
|
"cAICollections": {
|
||||||
|
"layout": "relationships/cAICollections",
|
||||||
|
"selectPrimaryFilterName": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"kanbanViewMode": false,
|
"kanbanViewMode": false,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
10
custom/Espo/Custom/Services/CAICollectionCDokumente.php
Normal file
10
custom/Espo/Custom/Services/CAICollectionCDokumente.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Espo\Custom\Services;
|
||||||
|
|
||||||
|
use Espo\Services\Record;
|
||||||
|
|
||||||
|
class CAICollectionCDokumente extends Record
|
||||||
|
{
|
||||||
|
// Keine zusätzliche Logik nötig
|
||||||
|
}
|
||||||
286
custom/docs/CHANGELOG_2026-03-09.md
Normal file
286
custom/docs/CHANGELOG_2026-03-09.md
Normal 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.
|
||||||
1698
custom/docs/ESPOCRM_BEST_PRACTICES.md
Normal file
1698
custom/docs/ESPOCRM_BEST_PRACTICES.md
Normal file
File diff suppressed because it is too large
Load Diff
319
custom/docs/README.md
Normal file
319
custom/docs/README.md
Normal 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`
|
||||||
592
custom/docs/TESTERGEBNISSE_JUNCTION_TABLE.md
Normal file
592
custom/docs/TESTERGEBNISSE_JUNCTION_TABLE.md
Normal 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)
|
||||||
72
custom/docs/workflows/README.md
Normal file
72
custom/docs/workflows/README.md
Normal 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
|
||||||
BIN
custom/scripts/__pycache__/validate_and_rebuild.cpython-311.pyc
Normal file
BIN
custom/scripts/__pycache__/validate_and_rebuild.cpython-311.pyc
Normal file
Binary file not shown.
80
custom/scripts/check_db_direct.php
Normal file
80
custom/scripts/check_db_direct.php
Normal 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";
|
||||||
|
}
|
||||||
70
custom/scripts/check_junction_table.php
Normal file
70
custom/scripts/check_junction_table.php
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
138
custom/scripts/junctiontabletests/test_api_final.py
Normal file
138
custom/scripts/junctiontabletests/test_api_final.py
Normal 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")
|
||||||
89
custom/scripts/junctiontabletests/test_api_params.py
Normal file
89
custom/scripts/junctiontabletests/test_api_params.py
Normal 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)
|
||||||
120
custom/scripts/junctiontabletests/test_api_simplified.py
Normal file
120
custom/scripts/junctiontabletests/test_api_simplified.py
Normal 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")
|
||||||
132
custom/scripts/junctiontabletests/test_api_variations.py
Normal file
132
custom/scripts/junctiontabletests/test_api_variations.py
Normal 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")
|
||||||
178
custom/scripts/junctiontabletests/test_junction_api.py
Normal file
178
custom/scripts/junctiontabletests/test_junction_api.py
Normal 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)
|
||||||
@@ -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")
|
||||||
190
custom/scripts/test_junction_internal.php
Normal file
190
custom/scripts/test_junction_internal.php
Normal 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);
|
||||||
@@ -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)")
|
||||||
|
|||||||
@@ -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**
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user