Compare commits
55 Commits
752969ec39
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a302542b7 | |||
| 7d55075490 | |||
| 867da15823 | |||
| 0abd37d7a5 | |||
| 7abd2122fe | |||
| cb3da68673 | |||
| ea4738d9eb | |||
| 672645673f | |||
| 22665948e4 | |||
| 0b829e9dfe | |||
| faffe3d874 | |||
| bf0f596ad4 | |||
| 3ecc6275bc | |||
| d0397e475e | |||
| 51d9f7fa22 | |||
| 80dc3b40d3 | |||
| e15dd14cab | |||
| 54d66da52d | |||
| ae359048af | |||
| c678660ad6 | |||
| c952fc40bc | |||
| b2c391539d | |||
| e7b14406fb | |||
| 4707925917 | |||
| c2c9cfe709 | |||
| 9411337939 | |||
| 986cafcfd6 | |||
| c12577f4f8 | |||
| f7b1adc015 | |||
| 0f307c7eca | |||
| 9ab8f8b4bf | |||
| 8438af8f97 | |||
| 76c38e8ad4 | |||
| c2766ec66a | |||
| 9b18a63acf | |||
| 641e5c0a91 | |||
| 3470dba301 | |||
| 0340c59e5c | |||
| cd7c80af0f | |||
| 1f32fbb89b | |||
| 63e3841f86 | |||
| 2e9db78c6e | |||
| 3361cffb14 | |||
| 47634c81ef | |||
| 53dd8f33d4 | |||
| 1b904eb15f | |||
| 3547f47fc3 | |||
| 1d3eb86c6e | |||
| 926b6d9719 | |||
| 81a690277f | |||
| ded721a9d4 | |||
| 218b6e0d97 | |||
| 845a55d170 | |||
| 6696f3f3c2 | |||
| 22ac828747 |
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.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
/************************************************************************
|
||||||
|
* This file is part of EspoCRM.
|
||||||
|
*
|
||||||
|
* EspoCRM – Open Source CRM application.
|
||||||
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
|
* Website: https://www.espocrm.com
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* The interactive user interfaces in modified source and object code versions
|
||||||
|
* of this program must display Appropriate Legal Notices, as required under
|
||||||
|
* Section 5 of the GNU Affero General Public License version 3.
|
||||||
|
*
|
||||||
|
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||||
|
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
namespace Espo\Classes\FieldValidators\Common\Host;
|
||||||
|
|
||||||
|
use Espo\Core\FieldValidation\Validator;
|
||||||
|
use Espo\Core\FieldValidation\Validator\Data;
|
||||||
|
use Espo\Core\FieldValidation\Validator\Failure;
|
||||||
|
use Espo\Core\Utils\Security\HostCheck;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements Validator<Entity>
|
||||||
|
* @since 9.3.2
|
||||||
|
*/
|
||||||
|
class NotInternal implements Validator
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private HostCheck $hostCheck,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function validate(Entity $entity, string $field, Data $data): ?Failure
|
||||||
|
{
|
||||||
|
$value = $entity->get($field);
|
||||||
|
|
||||||
|
if (!$value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->hostCheck->isHostAndNotInternal($value)) {
|
||||||
|
return Failure::create();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
/************************************************************************
|
||||||
|
* This file is part of EspoCRM.
|
||||||
|
*
|
||||||
|
* EspoCRM – Open Source CRM application.
|
||||||
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
|
* Website: https://www.espocrm.com
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* The interactive user interfaces in modified source and object code versions
|
||||||
|
* of this program must display Appropriate Legal Notices, as required under
|
||||||
|
* Section 5 of the GNU Affero General Public License version 3.
|
||||||
|
*
|
||||||
|
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||||
|
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
namespace Espo\Classes\FieldValidators\Webhook\Url;
|
||||||
|
|
||||||
|
use Espo\Core\FieldValidation\Validator;
|
||||||
|
use Espo\Core\FieldValidation\Validator\Data;
|
||||||
|
use Espo\Core\FieldValidation\Validator\Failure;
|
||||||
|
use Espo\Core\Utils\Security\UrlCheck;
|
||||||
|
use Espo\Core\Webhook\AddressUtil;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements Validator<Entity>
|
||||||
|
*/
|
||||||
|
class NotInternal implements Validator
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private UrlCheck $urlCheck,
|
||||||
|
private AddressUtil $addressUtil,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function validate(Entity $entity, string $field, Data $data): ?Failure
|
||||||
|
{
|
||||||
|
$value = $entity->get($field);
|
||||||
|
|
||||||
|
if (!$value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->urlCheck->isUrl($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->addressUtil->isAllowedUrl($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->urlCheck->isUrlAndNotIternal($value)) {
|
||||||
|
return Failure::create();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -178,7 +178,9 @@ class MassUpdate implements MassAction
|
|||||||
|
|
||||||
private function clearRoleCache(string $id): void
|
private function clearRoleCache(string $id): void
|
||||||
{
|
{
|
||||||
$this->fileManager->removeFile('data/cache/application/acl/' . $id . '.php');
|
$part = basename($id);
|
||||||
|
|
||||||
|
$this->fileManager->removeFile("data/cache/application/acl/$part.php");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function clearPortalRolesCache(): void
|
private function clearPortalRolesCache(): void
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ namespace Espo\Classes\RecordHooks\CurrencyRecordRate;
|
|||||||
use Espo\Core\Exceptions\Conflict;
|
use Espo\Core\Exceptions\Conflict;
|
||||||
use Espo\Core\Record\DeleteParams;
|
use Espo\Core\Record\DeleteParams;
|
||||||
use Espo\Core\Record\Hook\DeleteHook;
|
use Espo\Core\Record\Hook\DeleteHook;
|
||||||
|
use Espo\Core\Utils\Currency\DatabasePopulator;
|
||||||
use Espo\Core\WebSocket\Submission;
|
use Espo\Core\WebSocket\Submission;
|
||||||
use Espo\Entities\CurrencyRecordRate;
|
use Espo\Entities\CurrencyRecordRate;
|
||||||
use Espo\ORM\Entity;
|
use Espo\ORM\Entity;
|
||||||
@@ -46,6 +47,7 @@ class AfterDelete implements DeleteHook
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private SyncManager $syncManager,
|
private SyncManager $syncManager,
|
||||||
private Submission $submission,
|
private Submission $submission,
|
||||||
|
private DatabasePopulator $databasePopulator,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function process(Entity $entity, DeleteParams $params): void
|
public function process(Entity $entity, DeleteParams $params): void
|
||||||
@@ -58,6 +60,7 @@ class AfterDelete implements DeleteHook
|
|||||||
throw new Conflict($e->getMessage(), previous: $e);
|
throw new Conflict($e->getMessage(), previous: $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->databasePopulator->process();
|
||||||
$this->submission->submit('appParamsUpdate');
|
$this->submission->submit('appParamsUpdate');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace Espo\Classes\RecordHooks\CurrencyRecordRate;
|
|||||||
|
|
||||||
use Espo\Core\Exceptions\Conflict;
|
use Espo\Core\Exceptions\Conflict;
|
||||||
use Espo\Core\Record\Hook\SaveHook;
|
use Espo\Core\Record\Hook\SaveHook;
|
||||||
|
use Espo\Core\Utils\Currency\DatabasePopulator;
|
||||||
use Espo\Core\WebSocket\Submission;
|
use Espo\Core\WebSocket\Submission;
|
||||||
use Espo\Entities\CurrencyRecordRate;
|
use Espo\Entities\CurrencyRecordRate;
|
||||||
use Espo\ORM\Entity;
|
use Espo\ORM\Entity;
|
||||||
@@ -45,6 +46,7 @@ class AfterSave implements SaveHook
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private SyncManager $syncManager,
|
private SyncManager $syncManager,
|
||||||
private Submission $submission,
|
private Submission $submission,
|
||||||
|
private DatabasePopulator $databasePopulator,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function process(Entity $entity): void
|
public function process(Entity $entity): void
|
||||||
@@ -57,6 +59,7 @@ class AfterSave implements SaveHook
|
|||||||
throw new Conflict($e->getMessage(), previous: $e);
|
throw new Conflict($e->getMessage(), previous: $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->databasePopulator->process();
|
||||||
$this->submission->submit('appParamsUpdate');
|
$this->submission->submit('appParamsUpdate');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
/************************************************************************
|
||||||
|
* This file is part of EspoCRM.
|
||||||
|
*
|
||||||
|
* EspoCRM – Open Source CRM application.
|
||||||
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
|
* Website: https://www.espocrm.com
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* The interactive user interfaces in modified source and object code versions
|
||||||
|
* of this program must display Appropriate Legal Notices, as required under
|
||||||
|
* Section 5 of the GNU Affero General Public License version 3.
|
||||||
|
*
|
||||||
|
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||||
|
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
namespace Espo\Classes\RecordHooks\EmailAccount;
|
||||||
|
|
||||||
|
use Espo\Core\Exceptions\Forbidden;
|
||||||
|
use Espo\Core\Record\Hook\SaveHook;
|
||||||
|
use Espo\Core\Utils\Config;
|
||||||
|
use Espo\Core\Utils\Security\HostCheck;
|
||||||
|
use Espo\Entities\EmailAccount;
|
||||||
|
use Espo\Entities\InboundEmail;
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements SaveHook<EmailAccount|InboundEmail>
|
||||||
|
*/
|
||||||
|
class BeforeSaveValidateHosts implements SaveHook
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Config $config,
|
||||||
|
private HostCheck $hostCheck,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Entity $entity): void
|
||||||
|
{
|
||||||
|
if ($entity->isAttributeChanged('host') || $entity->isAttributeChanged('port')) {
|
||||||
|
$this->validateImap($entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entity->isAttributeChanged('smtpHost') || $entity->isAttributeChanged('smtpPort')) {
|
||||||
|
$this->validateSmtp($entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function validateImap(EmailAccount|InboundEmail $entity): void
|
||||||
|
{
|
||||||
|
$host = $entity->getHost();
|
||||||
|
$port = $entity->getPort();
|
||||||
|
|
||||||
|
if ($host === null || $port === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$address = $host . ':' . $port;
|
||||||
|
|
||||||
|
if (in_array($address, $this->getAllowedAddressList())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->hostCheck->isHostAndNotInternal($host)) {
|
||||||
|
$message = $this->composeErrorMessage($host, $address);
|
||||||
|
|
||||||
|
throw new Forbidden($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function validateSmtp(EmailAccount|InboundEmail $entity): void
|
||||||
|
{
|
||||||
|
$host = $entity->getSmtpHost();
|
||||||
|
$port = $entity->getSmtpPort();
|
||||||
|
|
||||||
|
if ($host === null || $port === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$address = $host . ':' . $port;
|
||||||
|
|
||||||
|
if (in_array($address, $this->getAllowedAddressList())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->hostCheck->isHostAndNotInternal($host)) {
|
||||||
|
$message = $this->composeErrorMessage($host, $address);
|
||||||
|
|
||||||
|
throw new Forbidden($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private function getAllowedAddressList(): array
|
||||||
|
{
|
||||||
|
return $this->config->get('emailServerAllowedAddressList') ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function composeErrorMessage(string $host, string $address): string
|
||||||
|
{
|
||||||
|
return "Host '$host' is not allowed as it's internal. " .
|
||||||
|
"To allow, add `$address` to the config parameter `emailServerAllowedAddressList`.";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ namespace Espo\Classes\TemplateHelpers;
|
|||||||
use Espo\Core\Htmlizer\Helper;
|
use Espo\Core\Htmlizer\Helper;
|
||||||
use Espo\Core\Htmlizer\Helper\Data;
|
use Espo\Core\Htmlizer\Helper\Data;
|
||||||
use Espo\Core\Htmlizer\Helper\Result;
|
use Espo\Core\Htmlizer\Helper\Result;
|
||||||
use Michelf\MarkdownExtra as MarkdownTransformer;
|
use Espo\Core\Utils\Markdown\Markdown;
|
||||||
|
|
||||||
class MarkdownText implements Helper
|
class MarkdownText implements Helper
|
||||||
{
|
{
|
||||||
@@ -44,7 +44,7 @@ class MarkdownText implements Helper
|
|||||||
return Result::createEmpty();
|
return Result::createEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
$transformed = MarkdownTransformer::defaultTransform($value);
|
$transformed = Markdown::transform($value);
|
||||||
|
|
||||||
return Result::createSafeString($transformed);
|
return Result::createSafeString($transformed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
namespace Espo\Controllers;
|
namespace Espo\Controllers;
|
||||||
|
|
||||||
use Espo\Core\Exceptions\Error;
|
use Espo\Core\Exceptions\Error;
|
||||||
|
use Espo\Core\Exceptions\Forbidden;
|
||||||
use Espo\Core\Mail\Account\GroupAccount\Service;
|
use Espo\Core\Mail\Account\GroupAccount\Service;
|
||||||
use Espo\Core\Mail\Account\Storage\Params as StorageParams;
|
use Espo\Core\Mail\Account\Storage\Params as StorageParams;
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ class InboundEmail extends Record
|
|||||||
* @return string[]
|
* @return string[]
|
||||||
* @throws Error
|
* @throws Error
|
||||||
* @throws ImapError
|
* @throws ImapError
|
||||||
|
* @throws Forbidden
|
||||||
*/
|
*/
|
||||||
public function postActionGetFolders(Request $request): array
|
public function postActionGetFolders(Request $request): array
|
||||||
{
|
{
|
||||||
@@ -67,6 +69,7 @@ class InboundEmail extends Record
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Error
|
* @throws Error
|
||||||
|
* @throws Forbidden
|
||||||
*/
|
*/
|
||||||
public function postActionTestConnection(Request $request): bool
|
public function postActionTestConnection(Request $request): bool
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -70,13 +70,6 @@ class Settings
|
|||||||
|
|
||||||
private function getConfigData(): stdClass
|
private function getConfigData(): stdClass
|
||||||
{
|
{
|
||||||
$data = $this->service->getConfigData();
|
return $this->service->getConfigData();
|
||||||
$metadataData = $this->service->getMetadataConfigData();
|
|
||||||
|
|
||||||
foreach (get_object_vars($metadataData) as $key => $value) {
|
|
||||||
$data->$key = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class Clearer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$part = $user->getId() . '.php';
|
$part = basename($user->getId() . '.php');
|
||||||
|
|
||||||
$this->fileManager->remove('data/cache/application/acl/' . $part);
|
$this->fileManager->remove('data/cache/application/acl/' . $part);
|
||||||
$this->fileManager->remove('data/cache/application/aclMap/' . $part);
|
$this->fileManager->remove('data/cache/application/aclMap/' . $part);
|
||||||
@@ -77,7 +77,7 @@ class Clearer
|
|||||||
->find();
|
->find();
|
||||||
|
|
||||||
foreach ($portals as $portal) {
|
foreach ($portals as $portal) {
|
||||||
$part = $portal->getId() . '/' . $user->getId() . '.php';
|
$part = basename($portal->getId()) . '/' . basename($user->getId() . '.php');
|
||||||
|
|
||||||
$this->fileManager->remove('data/cache/application/aclPortal/' . $part);
|
$this->fileManager->remove('data/cache/application/aclPortal/' . $part);
|
||||||
$this->fileManager->remove('data/cache/application/aclPortalMap/' . $part);
|
$this->fileManager->remove('data/cache/application/aclPortalMap/' . $part);
|
||||||
|
|||||||
@@ -115,7 +115,8 @@ class EspoUploadDir implements Storage, Local
|
|||||||
protected function getFilePath(Attachment $attachment)
|
protected function getFilePath(Attachment $attachment)
|
||||||
{
|
{
|
||||||
$sourceId = $attachment->getSourceId();
|
$sourceId = $attachment->getSourceId();
|
||||||
|
$file = basename($sourceId);
|
||||||
|
|
||||||
return 'data/upload/' . $sourceId;
|
return 'data/upload/' . $file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ use Espo\Core\Formula\EvaluatedArgumentList;
|
|||||||
use Espo\Core\Formula\Exceptions\BadArgumentType;
|
use Espo\Core\Formula\Exceptions\BadArgumentType;
|
||||||
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
use Espo\Core\Formula\Exceptions\TooFewArguments;
|
||||||
use Espo\Core\Formula\Func;
|
use Espo\Core\Formula\Func;
|
||||||
use Michelf\Markdown;
|
use Espo\Core\Utils\Markdown\Markdown;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @noinspection PhpUnused
|
* @noinspection PhpUnused
|
||||||
@@ -52,6 +52,6 @@ class TransformType implements Func
|
|||||||
throw BadArgumentType::create(1, 'string');
|
throw BadArgumentType::create(1, 'string');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Markdown::defaultTransform($string);
|
return Markdown::transform($string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,16 +30,19 @@
|
|||||||
namespace Espo\Core\Mail\Account\GroupAccount;
|
namespace Espo\Core\Mail\Account\GroupAccount;
|
||||||
|
|
||||||
use Espo\Core\Exceptions\ErrorSilent;
|
use Espo\Core\Exceptions\ErrorSilent;
|
||||||
|
use Espo\Core\Exceptions\Forbidden;
|
||||||
use Espo\Core\Mail\Account\Account as Account;
|
use Espo\Core\Mail\Account\Account as Account;
|
||||||
use Espo\Core\Exceptions\Error;
|
use Espo\Core\Exceptions\Error;
|
||||||
use Espo\Core\Mail\Account\Fetcher;
|
use Espo\Core\Mail\Account\Fetcher;
|
||||||
use Espo\Core\Mail\Account\Storage\Params;
|
use Espo\Core\Mail\Account\Storage\Params;
|
||||||
use Espo\Core\Mail\Account\StorageFactory;
|
use Espo\Core\Mail\Account\StorageFactory;
|
||||||
|
use Espo\Core\Mail\Account\Util\AddressUtil;
|
||||||
use Espo\Core\Mail\Account\Util\NotificationHelper;
|
use Espo\Core\Mail\Account\Util\NotificationHelper;
|
||||||
use Espo\Core\Mail\Exceptions\ImapError;
|
use Espo\Core\Mail\Exceptions\ImapError;
|
||||||
use Espo\Core\Mail\Exceptions\NoImap;
|
use Espo\Core\Mail\Exceptions\NoImap;
|
||||||
use Espo\Core\Mail\Sender\Message;
|
use Espo\Core\Mail\Sender\Message;
|
||||||
use Espo\Core\Utils\Log;
|
use Espo\Core\Utils\Log;
|
||||||
|
use Espo\Core\Utils\Security\HostCheck;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class Service
|
class Service
|
||||||
@@ -49,7 +52,9 @@ class Service
|
|||||||
private AccountFactory $accountFactory,
|
private AccountFactory $accountFactory,
|
||||||
private StorageFactory $storageFactory,
|
private StorageFactory $storageFactory,
|
||||||
private Log $log,
|
private Log $log,
|
||||||
private NotificationHelper $notificationHelper
|
private NotificationHelper $notificationHelper,
|
||||||
|
private HostCheck $hostCheck,
|
||||||
|
private AddressUtil $addressUtil,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,9 +82,18 @@ class Service
|
|||||||
* @return string[]
|
* @return string[]
|
||||||
* @throws Error
|
* @throws Error
|
||||||
* @throws ImapError
|
* @throws ImapError
|
||||||
|
* @throws Forbidden
|
||||||
*/
|
*/
|
||||||
public function getFolderList(Params $params): array
|
public function getFolderList(Params $params): array
|
||||||
{
|
{
|
||||||
|
if (
|
||||||
|
$params->getHost() &&
|
||||||
|
!$this->addressUtil->isAllowedAddress($params) &&
|
||||||
|
!$this->hostCheck->isHostAndNotInternal($params->getHost())
|
||||||
|
) {
|
||||||
|
throw new Forbidden("Not allowed internal host.");
|
||||||
|
}
|
||||||
|
|
||||||
if ($params->getId()) {
|
if ($params->getId()) {
|
||||||
$account = $this->accountFactory->create($params->getId());
|
$account = $this->accountFactory->create($params->getId());
|
||||||
|
|
||||||
@@ -95,6 +109,7 @@ class Service
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Error
|
* @throws Error
|
||||||
|
* @throws Forbidden
|
||||||
*/
|
*/
|
||||||
public function testConnection(Params $params): void
|
public function testConnection(Params $params): void
|
||||||
{
|
{
|
||||||
@@ -106,6 +121,14 @@ class Service
|
|||||||
->withImapHandlerClassName($account->getImapHandlerClassName());
|
->withImapHandlerClassName($account->getImapHandlerClassName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$params->getHost() &&
|
||||||
|
!$this->addressUtil->isAllowedAddress($params) &&
|
||||||
|
!$this->hostCheck->isHostAndNotInternal($params->getHost())
|
||||||
|
) {
|
||||||
|
throw new Forbidden("Not allowed internal host.");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$storage = $this->storageFactory->createWithParams($params);
|
$storage = $this->storageFactory->createWithParams($params);
|
||||||
$storage->getFolderNames();
|
$storage->getFolderNames();
|
||||||
|
|||||||
@@ -30,9 +30,11 @@
|
|||||||
namespace Espo\Core\Mail\Account\PersonalAccount;
|
namespace Espo\Core\Mail\Account\PersonalAccount;
|
||||||
|
|
||||||
use Espo\Core\Exceptions\ErrorSilent;
|
use Espo\Core\Exceptions\ErrorSilent;
|
||||||
|
use Espo\Core\Mail\Account\Util\AddressUtil;
|
||||||
use Espo\Core\Mail\Account\Util\NotificationHelper;
|
use Espo\Core\Mail\Account\Util\NotificationHelper;
|
||||||
use Espo\Core\Mail\Exceptions\ImapError;
|
use Espo\Core\Mail\Exceptions\ImapError;
|
||||||
use Espo\Core\Mail\Exceptions\NoImap;
|
use Espo\Core\Mail\Exceptions\NoImap;
|
||||||
|
use Espo\Core\Utils\Config;
|
||||||
use Espo\Core\Utils\Log;
|
use Espo\Core\Utils\Log;
|
||||||
use Espo\Core\Mail\Account\Account as Account;
|
use Espo\Core\Mail\Account\Account as Account;
|
||||||
use Espo\Core\Exceptions\Forbidden;
|
use Espo\Core\Exceptions\Forbidden;
|
||||||
@@ -40,6 +42,7 @@ use Espo\Core\Exceptions\Error;
|
|||||||
use Espo\Core\Mail\Account\Fetcher;
|
use Espo\Core\Mail\Account\Fetcher;
|
||||||
use Espo\Core\Mail\Account\Storage\Params;
|
use Espo\Core\Mail\Account\Storage\Params;
|
||||||
use Espo\Core\Mail\Account\StorageFactory;
|
use Espo\Core\Mail\Account\StorageFactory;
|
||||||
|
use Espo\Core\Utils\Security\HostCheck;
|
||||||
use Espo\Entities\User;
|
use Espo\Entities\User;
|
||||||
use Espo\Core\Mail\Sender\Message;
|
use Espo\Core\Mail\Sender\Message;
|
||||||
|
|
||||||
@@ -53,7 +56,9 @@ class Service
|
|||||||
private StorageFactory $storageFactory,
|
private StorageFactory $storageFactory,
|
||||||
private User $user,
|
private User $user,
|
||||||
private Log $log,
|
private Log $log,
|
||||||
private NotificationHelper $notificationHelper
|
private NotificationHelper $notificationHelper,
|
||||||
|
private HostCheck $hostCheck,
|
||||||
|
private AddressUtil $addressUtil,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,6 +100,14 @@ class Service
|
|||||||
throw new Forbidden();
|
throw new Forbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$params->getHost() &&
|
||||||
|
!$this->addressUtil->isAllowedAddress($params) &&
|
||||||
|
!$this->hostCheck->isHostAndNotInternal($params->getHost())
|
||||||
|
) {
|
||||||
|
throw new Forbidden("Not allowed internal host.");
|
||||||
|
}
|
||||||
|
|
||||||
if ($params->getId()) {
|
if ($params->getId()) {
|
||||||
$account = $this->accountFactory->create($params->getId());
|
$account = $this->accountFactory->create($params->getId());
|
||||||
|
|
||||||
@@ -128,6 +141,14 @@ class Service
|
|||||||
throw new Forbidden();
|
throw new Forbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$params->getHost() &&
|
||||||
|
!$this->addressUtil->isAllowedAddress($params) &&
|
||||||
|
!$this->hostCheck->isHostAndNotInternal($params->getHost())
|
||||||
|
) {
|
||||||
|
throw new Forbidden("Not allowed host.");
|
||||||
|
}
|
||||||
|
|
||||||
if ($params->getId()) {
|
if ($params->getId()) {
|
||||||
$account = $this->accountFactory->create($params->getId());
|
$account = $this->accountFactory->create($params->getId());
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* This file is part of EspoCRM.
|
* This file is part of EspoCRM.
|
||||||
*
|
*
|
||||||
* EspoCRM – Open Source CRM application.
|
* EspoCRM – Open Source CRM application.
|
||||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
* Website: https://www.espocrm.com
|
* Website: https://www.espocrm.com
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
@@ -27,44 +27,43 @@
|
|||||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||||
************************************************************************/
|
************************************************************************/
|
||||||
|
|
||||||
namespace Espo\Tools\UserSecurity\Password\Recovery;
|
namespace Espo\Core\Mail\Account\Util;
|
||||||
|
|
||||||
use Espo\Core\Exceptions\Forbidden;
|
use Espo\Core\Mail\Account\Storage\Params;
|
||||||
|
use Espo\Core\Mail\SmtpParams;
|
||||||
use Espo\Core\Utils\Config;
|
use Espo\Core\Utils\Config;
|
||||||
use Espo\Entities\Portal;
|
|
||||||
use Espo\ORM\EntityManager;
|
|
||||||
|
|
||||||
class UrlValidator
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class AddressUtil
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private Config $config,
|
private Config $config,
|
||||||
private EntityManager $entityManager
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Forbidden
|
* @internal
|
||||||
*/
|
*/
|
||||||
public function validate(string $url): void
|
public function isAllowedAddress(Params|SmtpParams $params): bool
|
||||||
{
|
{
|
||||||
$siteUrl = rtrim($this->config->get('siteUrl') ?? '', '/');
|
$host = $params instanceof Params ? $params->getHost() : $params->getServer();
|
||||||
|
$port = $params->getPort();
|
||||||
|
|
||||||
if (str_starts_with($url, $siteUrl)) {
|
if ($port === null || !$host) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var iterable<Portal> $portals */
|
$address = $host . ':' . $port;
|
||||||
$portals = $this->entityManager
|
|
||||||
->getRDBRepositoryByClass(Portal::class)
|
|
||||||
->find();
|
|
||||||
|
|
||||||
foreach ($portals as $portal) {
|
return in_array($address, $this->getAllowedAddressList());
|
||||||
$siteUrl = rtrim($portal->getUrl() ?? '', '/');
|
}
|
||||||
|
|
||||||
if (str_starts_with($url, $siteUrl)) {
|
/**
|
||||||
return;
|
* @return string[]
|
||||||
}
|
*/
|
||||||
}
|
private function getAllowedAddressList(): array
|
||||||
|
{
|
||||||
throw new Forbidden("URL does not match Site URL.");
|
return $this->config->get('emailServerAllowedAddressList') ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,8 +420,8 @@ class DefaultImporter implements Importer
|
|||||||
$subject = '(No Subject)';
|
$subject = '(No Subject)';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strlen($subject) > self::SUBJECT_MAX_LENGTH) {
|
if (mb_strlen($subject) > self::SUBJECT_MAX_LENGTH) {
|
||||||
$subject = substr($subject, 0, self::SUBJECT_MAX_LENGTH);
|
$subject = mb_substr($subject, 0, self::SUBJECT_MAX_LENGTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $subject;
|
return $subject;
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ class Starter extends StarterBase
|
|||||||
SystemConfig $systemConfig,
|
SystemConfig $systemConfig,
|
||||||
ApplicationState $applicationState
|
ApplicationState $applicationState
|
||||||
) {
|
) {
|
||||||
$routeCacheFile = 'data/cache/application/slim-routes-portal-' . $applicationState->getPortalId() . '.php';
|
$part = basename($applicationState->getPortalId());
|
||||||
|
|
||||||
|
$routeCacheFile = 'data/cache/application/slim-routes-portal-' . $part . '.php';
|
||||||
|
|
||||||
parent::__construct(
|
parent::__construct(
|
||||||
$requestProcessor,
|
$requestProcessor,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* This file is part of EspoCRM.
|
* This file is part of EspoCRM.
|
||||||
*
|
*
|
||||||
* EspoCRM – Open Source CRM application.
|
* EspoCRM – Open Source CRM application.
|
||||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
* Website: https://www.espocrm.com
|
* Website: https://www.espocrm.com
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
@@ -27,26 +27,24 @@
|
|||||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||||
************************************************************************/
|
************************************************************************/
|
||||||
|
|
||||||
namespace Espo\Core\Utils\Database\Dbal\Platforms\Keywords;
|
namespace Espo\Core\Utils\Markdown;
|
||||||
|
|
||||||
use Doctrine\DBAL\Platforms\Keywords\MariaDBKeywords;
|
use Michelf\Markdown as MarkdownParser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 'LEAD' happened to be a reserved words on some environments.
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class MariaDb102Keywords extends MariaDBKeywords
|
class Markdown
|
||||||
{
|
{
|
||||||
/** @deprecated */
|
/**
|
||||||
public function getName(): string
|
* @internal
|
||||||
|
*/
|
||||||
|
public static function transform(string $text): string
|
||||||
{
|
{
|
||||||
return 'MariaDb102';
|
$parser = new MarkdownParser();
|
||||||
}
|
$parser->no_markup = true;
|
||||||
|
$parser->no_entities = true;
|
||||||
|
|
||||||
protected function getKeywords(): array
|
return $parser->transform($text);
|
||||||
{
|
|
||||||
return [
|
|
||||||
...parent::getKeywords(),
|
|
||||||
'LEAD',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
225
application/Espo/Core/Utils/Security/HostCheck.php
Normal file
225
application/Espo/Core/Utils/Security/HostCheck.php
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
<?php
|
||||||
|
/************************************************************************
|
||||||
|
* This file is part of EspoCRM.
|
||||||
|
*
|
||||||
|
* EspoCRM – Open Source CRM application.
|
||||||
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
|
* Website: https://www.espocrm.com
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* The interactive user interfaces in modified source and object code versions
|
||||||
|
* of this program must display Appropriate Legal Notices, as required under
|
||||||
|
* Section 5 of the GNU Affero General Public License version 3.
|
||||||
|
*
|
||||||
|
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||||
|
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
namespace Espo\Core\Utils\Security;
|
||||||
|
|
||||||
|
use const DNS_A;
|
||||||
|
use const FILTER_FLAG_NO_PRIV_RANGE;
|
||||||
|
use const FILTER_FLAG_NO_RES_RANGE;
|
||||||
|
use const FILTER_FLAG_HOSTNAME;
|
||||||
|
use const FILTER_VALIDATE_DOMAIN;
|
||||||
|
use const FILTER_VALIDATE_IP;
|
||||||
|
|
||||||
|
class HostCheck
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validates the string is a host and it's not internal.
|
||||||
|
* If not a host, returns false.
|
||||||
|
*
|
||||||
|
* @since 9.3.4
|
||||||
|
*/
|
||||||
|
public function isHostAndNotInternal(string $host): bool
|
||||||
|
{
|
||||||
|
if (filter_var($host, FILTER_VALIDATE_IP)) {
|
||||||
|
return $this->ipAddressIsNotInternal($host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isDomainHost($host)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipAddresses = $this->getHostIpAddresses($host);
|
||||||
|
|
||||||
|
if ($ipAddresses === []) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($ipAddresses as $idAddress) {
|
||||||
|
if (!$this->ipAddressIsNotInternal($idAddress)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @since 9.3.4
|
||||||
|
*/
|
||||||
|
public function isDomainHost(string $host): bool
|
||||||
|
{
|
||||||
|
$normalized = $this->normalizeIpAddress($host);
|
||||||
|
|
||||||
|
if ($normalized !== false && filter_var($normalized, FILTER_VALIDATE_IP)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->hasNoNumericItem($host)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter_var($host, FILTER_VALIDATE_DOMAIN)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
* @internal
|
||||||
|
* @since 9.3.4
|
||||||
|
*/
|
||||||
|
public function getHostIpAddresses(string $host): array
|
||||||
|
{
|
||||||
|
$records = dns_get_record($host, DNS_A);
|
||||||
|
|
||||||
|
if (!$records) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = [];
|
||||||
|
|
||||||
|
foreach ($records as $record) {
|
||||||
|
/** @var ?string $idAddress */
|
||||||
|
$idAddress = $record['ip'] ?? null;
|
||||||
|
|
||||||
|
if (!$idAddress) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output[] = $idAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function ipAddressIsNotInternal(string $ipAddress): bool
|
||||||
|
{
|
||||||
|
return (bool) filter_var(
|
||||||
|
$ipAddress,
|
||||||
|
FILTER_VALIDATE_IP,
|
||||||
|
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Since 9.3.4. Use `isHostAndNotInternal`.
|
||||||
|
* @todo Remove in 9.4.0.
|
||||||
|
*/
|
||||||
|
public function isNotInternalHost(string $host): bool
|
||||||
|
{
|
||||||
|
return $this->isHostAndNotInternal($host);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeIpAddress(string $ip): string|false
|
||||||
|
{
|
||||||
|
if (!str_contains($ip, '.')) {
|
||||||
|
return self::normalizePart($ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode('.', $ip);
|
||||||
|
|
||||||
|
if (count($parts) !== 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if (preg_match('/^0x[0-9a-f]+$/i', $part)) {
|
||||||
|
$num = hexdec($part);
|
||||||
|
} else if (preg_match('/^0[0-7]+$/', $part) && $part !== '0') {
|
||||||
|
$num = octdec($part);
|
||||||
|
} else if (ctype_digit($part)) {
|
||||||
|
$num = (int)$part;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($num < 0 || $num > 255) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = $num;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode('.', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizePart(string $ip): string|false
|
||||||
|
{
|
||||||
|
if (preg_match('/^0x[0-9a-f]+$/i', $ip)) {
|
||||||
|
$num = hexdec($ip);
|
||||||
|
} elseif (preg_match('/^0[0-7]+$/', $ip) && $ip !== '0') {
|
||||||
|
$num = octdec($ip);
|
||||||
|
} elseif (ctype_digit($ip)) {
|
||||||
|
$num = (int) $ip;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($num < 0 || $num > 0xFFFFFFFF) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$num = (int) $num;
|
||||||
|
|
||||||
|
return long2ip($num);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function hasNoNumericItem(string $host): bool
|
||||||
|
{
|
||||||
|
$hasNoNumeric = false;
|
||||||
|
|
||||||
|
foreach (explode('.', $host) as $it) {
|
||||||
|
if (!is_numeric($it) && !self::isHex($it)) {
|
||||||
|
$hasNoNumeric = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hasNoNumeric;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isHex(string $value): bool
|
||||||
|
{
|
||||||
|
return preg_match('/^0x[0-9a-fA-F]+$/', $value) === 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,24 +29,23 @@
|
|||||||
|
|
||||||
namespace Espo\Core\Utils\Security;
|
namespace Espo\Core\Utils\Security;
|
||||||
|
|
||||||
use const DNS_A;
|
|
||||||
use const FILTER_FLAG_NO_PRIV_RANGE;
|
|
||||||
use const FILTER_FLAG_NO_RES_RANGE;
|
|
||||||
use const FILTER_VALIDATE_IP;
|
|
||||||
use const FILTER_VALIDATE_URL;
|
|
||||||
use const PHP_URL_HOST;
|
|
||||||
|
|
||||||
class UrlCheck
|
class UrlCheck
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private HostCheck $hostCheck,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function isUrl(string $url): bool
|
public function isUrl(string $url): bool
|
||||||
{
|
{
|
||||||
return filter_var($url, FILTER_VALIDATE_URL) !== false;
|
return filter_var($url, FILTER_VALIDATE_URL) !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a URL does not follow to an internal host.
|
* Checks whether it's a URL, and it does not follow to an internal host.
|
||||||
|
*
|
||||||
|
* @since 9.3.4
|
||||||
*/
|
*/
|
||||||
public function isNotInternalUrl(string $url): bool
|
public function isUrlAndNotIternal(string $url): bool
|
||||||
{
|
{
|
||||||
if (!$this->isUrl($url)) {
|
if (!$this->isUrl($url)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -58,38 +57,118 @@ class UrlCheck
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$records = dns_get_record($host, DNS_A);
|
return $this->hostCheck->isHostAndNotInternal($host);
|
||||||
|
}
|
||||||
|
|
||||||
if (filter_var($host, FILTER_VALIDATE_IP)) {
|
/**
|
||||||
return $this->ipAddressIsNotInternal($host);
|
* @return ?string[] Null if not a domain name or not a URL.
|
||||||
|
* @internal
|
||||||
|
* @since 9.3.4
|
||||||
|
*/
|
||||||
|
public function getCurlResolve(string $url): ?array
|
||||||
|
{
|
||||||
|
if (!$this->isUrl($url)) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$records) {
|
$host = parse_url($url, PHP_URL_HOST);
|
||||||
|
$port = parse_url($url, PHP_URL_PORT);
|
||||||
|
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||||
|
|
||||||
|
if ($port === null && $scheme) {
|
||||||
|
$port = match (strtolower($scheme)) {
|
||||||
|
'http' => 80,
|
||||||
|
'https'=> 443,
|
||||||
|
'ftp' => 21,
|
||||||
|
'ssh' => 22,
|
||||||
|
'smtp' => 25,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($port === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_string($host)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter_var($host, FILTER_VALIDATE_IP)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->hostCheck->isDomainHost($host)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipAddresses = $this->hostCheck->getHostIpAddresses($host);
|
||||||
|
|
||||||
|
$output = [];
|
||||||
|
|
||||||
|
foreach ($ipAddresses as $ipAddress) {
|
||||||
|
$ipPart = $ipAddress;
|
||||||
|
|
||||||
|
if (filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
$ipPart = "[$ipPart]";
|
||||||
|
}
|
||||||
|
|
||||||
|
$output[] = "$host:$port:$ipPart";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Since 9.3.4. Use `isUrlAndNotIternal`.
|
||||||
|
* @todo Remove in 9.5.0.
|
||||||
|
*/
|
||||||
|
public function isNotInternalUrl(string $url): bool
|
||||||
|
{
|
||||||
|
return $this->isUrlAndNotIternal($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $resolve
|
||||||
|
* @param string[] $allowed An allowed address list in the `{host}:{port}` format.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function validateCurlResolveNotInternal(array $resolve, array $allowed = []): bool
|
||||||
|
{
|
||||||
|
if ($resolve === []) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($records as $record) {
|
$ipAddresses = [];
|
||||||
/** @var ?string $idAddress */
|
|
||||||
$idAddress = $record['ip'] ?? null;
|
|
||||||
|
|
||||||
if (!$idAddress) {
|
foreach ($resolve as $item) {
|
||||||
|
$arr = explode(':', $item, 3);
|
||||||
|
|
||||||
|
if (count($arr) < 3) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->ipAddressIsNotInternal($idAddress)) {
|
$ipAddress = $arr[2];
|
||||||
|
$port = $arr[1];
|
||||||
|
$domain = $arr[0];
|
||||||
|
|
||||||
|
if (in_array("$ipAddress:$port", $allowed) || in_array("$domain:$port", $allowed)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($ipAddress, '[') && str_ends_with($ipAddress, ']')) {
|
||||||
|
$ipAddress = substr($ipAddress, 1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipAddresses[] = $ipAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($ipAddresses as $ipAddress) {
|
||||||
|
if (!$this->hostCheck->ipAddressIsNotInternal($ipAddress)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function ipAddressIsNotInternal(string $ipAddress): bool
|
|
||||||
{
|
|
||||||
return (bool) filter_var(
|
|
||||||
$ipAddress,
|
|
||||||
FILTER_VALIDATE_IP,
|
|
||||||
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,13 @@ class TemplateFileManager
|
|||||||
?string $entityType = null
|
?string $entityType = null
|
||||||
): string {
|
): string {
|
||||||
|
|
||||||
|
$type = basename($type);
|
||||||
|
$language = basename($language);
|
||||||
|
$name = basename($name);
|
||||||
|
|
||||||
if ($entityType) {
|
if ($entityType) {
|
||||||
|
$entityType = basename($entityType);
|
||||||
|
|
||||||
return "custom/Espo/Custom/Resources/templates/{$type}/{$language}/{$entityType}/{$name}.tpl";
|
return "custom/Espo/Custom/Resources/templates/{$type}/{$language}/{$entityType}/{$name}.tpl";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +158,13 @@ class TemplateFileManager
|
|||||||
?string $entityType = null
|
?string $entityType = null
|
||||||
): string {
|
): string {
|
||||||
|
|
||||||
|
$type = basename($type);
|
||||||
|
$language = basename($language);
|
||||||
|
$name = basename($name);
|
||||||
|
|
||||||
if ($entityType) {
|
if ($entityType) {
|
||||||
|
$entityType = basename($entityType);
|
||||||
|
|
||||||
return "templates/{$type}/{$language}/{$entityType}/{$name}.tpl";
|
return "templates/{$type}/{$language}/{$entityType}/{$name}.tpl";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
79
application/Espo/Core/Webhook/AddressUtil.php
Normal file
79
application/Espo/Core/Webhook/AddressUtil.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
/************************************************************************
|
||||||
|
* This file is part of EspoCRM.
|
||||||
|
*
|
||||||
|
* EspoCRM – Open Source CRM application.
|
||||||
|
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||||
|
* Website: https://www.espocrm.com
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* The interactive user interfaces in modified source and object code versions
|
||||||
|
* of this program must display Appropriate Legal Notices, as required under
|
||||||
|
* Section 5 of the GNU Affero General Public License version 3.
|
||||||
|
*
|
||||||
|
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||||
|
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||||
|
************************************************************************/
|
||||||
|
|
||||||
|
namespace Espo\Core\Webhook;
|
||||||
|
|
||||||
|
use Espo\Core\Utils\Config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class AddressUtil
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Config $config,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function isAllowedUrl(string $url): bool
|
||||||
|
{
|
||||||
|
/** @var string[] $allowedAddressList */
|
||||||
|
$allowedAddressList = $this->config->get('webhookAllowedAddressList') ?? [];
|
||||||
|
|
||||||
|
if (!$allowedAddressList) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = parse_url($url, PHP_URL_HOST);
|
||||||
|
$port = parse_url($url, PHP_URL_PORT);
|
||||||
|
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||||
|
|
||||||
|
if (!is_string($host)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_int($port)) {
|
||||||
|
if ($scheme === 'https') {
|
||||||
|
$port = 443;
|
||||||
|
} else if ($scheme === 'http') {
|
||||||
|
$port = 80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_int($port)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$address = $host . ':' . $port;
|
||||||
|
|
||||||
|
return in_array($address, $allowedAddressList);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ namespace Espo\Core\Webhook;
|
|||||||
use Espo\Core\Exceptions\Error;
|
use Espo\Core\Exceptions\Error;
|
||||||
use Espo\Core\Utils\Config;
|
use Espo\Core\Utils\Config;
|
||||||
use Espo\Core\Utils\Json;
|
use Espo\Core\Utils\Json;
|
||||||
|
use Espo\Core\Utils\Security\UrlCheck;
|
||||||
use Espo\Entities\Webhook;
|
use Espo\Entities\Webhook;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,8 +43,11 @@ class Sender
|
|||||||
private const CONNECT_TIMEOUT = 5;
|
private const CONNECT_TIMEOUT = 5;
|
||||||
private const TIMEOUT = 10;
|
private const TIMEOUT = 10;
|
||||||
|
|
||||||
public function __construct(private Config $config)
|
public function __construct(
|
||||||
{}
|
private Config $config,
|
||||||
|
private UrlCheck $urlCheck,
|
||||||
|
private AddressUtil $addressUtil,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, mixed> $dataList
|
* @param array<int, mixed> $dataList
|
||||||
@@ -85,6 +89,30 @@ class Sender
|
|||||||
throw new Error("Webhook does not have URL.");
|
throw new Error("Webhook does not have URL.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->urlCheck->isUrl($url)) {
|
||||||
|
throw new Error("'$url' is not valid URL.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!$this->addressUtil->isAllowedUrl($url) &&
|
||||||
|
!$this->urlCheck->isUrlAndNotIternal($url)
|
||||||
|
) {
|
||||||
|
throw new Error("URL '$url' points to an internal host, not allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolve = $this->urlCheck->getCurlResolve($url);
|
||||||
|
|
||||||
|
if ($resolve === []) {
|
||||||
|
throw new Error("Could not resolve the host.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var string[] $allowedAddressList */
|
||||||
|
$allowedAddressList = $this->config->get('webhookAllowedAddressList') ?? [];
|
||||||
|
|
||||||
|
if ($resolve !== null && !$this->urlCheck->validateCurlResolveNotInternal($resolve, $allowedAddressList)) {
|
||||||
|
throw new Error("Forbidden host.");
|
||||||
|
}
|
||||||
|
|
||||||
$handler = curl_init($url);
|
$handler = curl_init($url);
|
||||||
|
|
||||||
if ($handler === false) {
|
if ($handler === false) {
|
||||||
@@ -103,6 +131,10 @@ class Sender
|
|||||||
curl_setopt($handler, \CURLOPT_HTTPHEADER, $headerList);
|
curl_setopt($handler, \CURLOPT_HTTPHEADER, $headerList);
|
||||||
curl_setopt($handler, \CURLOPT_POSTFIELDS, $payload);
|
curl_setopt($handler, \CURLOPT_POSTFIELDS, $payload);
|
||||||
|
|
||||||
|
if ($resolve) {
|
||||||
|
curl_setopt($handler, CURLOPT_RESOLVE, $resolve);
|
||||||
|
}
|
||||||
|
|
||||||
curl_exec($handler);
|
curl_exec($handler);
|
||||||
|
|
||||||
$code = curl_getinfo($handler, \CURLINFO_HTTP_CODE);
|
$code = curl_getinfo($handler, \CURLINFO_HTTP_CODE);
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class Attachment implements EntryPoint
|
|||||||
$response
|
$response
|
||||||
->setHeader('Content-Length', (string) $size)
|
->setHeader('Content-Length', (string) $size)
|
||||||
->setHeader('Cache-Control', 'private, max-age=864000, immutable')
|
->setHeader('Cache-Control', 'private, max-age=864000, immutable')
|
||||||
->setHeader('Content-Security-Policy', "default-src 'self'")
|
->setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';")
|
||||||
->setBody($stream);
|
->setBody($stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class Download implements EntryPoint
|
|||||||
if (in_array($type, $inlineMimeTypeList)) {
|
if (in_array($type, $inlineMimeTypeList)) {
|
||||||
$disposition = 'inline';
|
$disposition = 'inline';
|
||||||
|
|
||||||
$response->setHeader('Content-Security-Policy', "default-src 'self'");
|
$response->setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';");
|
||||||
}
|
}
|
||||||
|
|
||||||
$response->setHeader('Content-Description', 'File Transfer');
|
$response->setHeader('Content-Description', 'File Transfer');
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class Image implements EntryPoint
|
|||||||
$response
|
$response
|
||||||
->setHeader('Content-Disposition', 'inline;filename="' . $fileName . '"')
|
->setHeader('Content-Disposition', 'inline;filename="' . $fileName . '"')
|
||||||
->setHeader('Content-Length', (string) $fileSize)
|
->setHeader('Content-Length', (string) $fileSize)
|
||||||
->setHeader('Content-Security-Policy', "default-src 'self'");
|
->setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';");
|
||||||
|
|
||||||
if (!$noCacheHeaders) {
|
if (!$noCacheHeaders) {
|
||||||
$response->setHeader('Cache-Control', 'private, max-age=864000, immutable');
|
$response->setHeader('Cache-Control', 'private, max-age=864000, immutable');
|
||||||
@@ -174,7 +174,9 @@ class Image implements EntryPoint
|
|||||||
|
|
||||||
$sourceId = $attachment->getSourceId();
|
$sourceId = $attachment->getSourceId();
|
||||||
|
|
||||||
$cacheFilePath = "data/upload/thumbs/{$sourceId}_$size";
|
$file = basename("{$sourceId}_$size");
|
||||||
|
|
||||||
|
$cacheFilePath = "data/upload/thumbs/$file";
|
||||||
|
|
||||||
if ($useCache && $this->fileManager->isFile($cacheFilePath)) {
|
if ($useCache && $this->fileManager->isFile($cacheFilePath)) {
|
||||||
return $this->fileManager->getContents($cacheFilePath);
|
return $this->fileManager->getContents($cacheFilePath);
|
||||||
|
|||||||
@@ -89,7 +89,9 @@ class RemoveFile implements AfterRemove
|
|||||||
$sizeList = array_keys($this->metadata->get(['app', 'image', 'sizes']) ?? []);
|
$sizeList = array_keys($this->metadata->get(['app', 'image', 'sizes']) ?? []);
|
||||||
|
|
||||||
foreach ($sizeList as $size) {
|
foreach ($sizeList as $size) {
|
||||||
$filePath = "data/upload/thumbs/{$entity->getSourceId()}_{$size}";
|
$file = basename("{$entity->getSourceId()}_$size");
|
||||||
|
|
||||||
|
$filePath = "data/upload/thumbs/$file";
|
||||||
|
|
||||||
if ($this->fileManager->isFile($filePath)) {
|
if ($this->fileManager->isFile($filePath)) {
|
||||||
$this->fileManager->removeFile($filePath);
|
$this->fileManager->removeFile($filePath);
|
||||||
|
|||||||
@@ -221,7 +221,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"customizationOptionsDisabled": true,
|
"customizationOptionsDisabled": true,
|
||||||
"textFilterDisabled": true
|
"textFilterDisabled": true,
|
||||||
|
"importEnabled": true
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "text"
|
"type": "text"
|
||||||
|
|||||||
@@ -1,380 +1,380 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"Enabled": "Povoleno",
|
"Enabled": "Povoleno",
|
||||||
"Disabled": "Zakázáno",
|
"Disabled": "Zakázáno",
|
||||||
"System": "Systém",
|
"System": "Systém",
|
||||||
"Users": "Uživatelé",
|
"Users": "Uživatelé",
|
||||||
"Customization": "Přizpůsobení",
|
"Customization": "Přizpůsobení",
|
||||||
"Available Fields": "Dostupná pole",
|
"Available Fields": "Dostupná pole",
|
||||||
"Layout": "Vzhled",
|
"Layout": "Vzhled",
|
||||||
"Entity Manager": "Správa entit",
|
"Entity Manager": "Správa entit",
|
||||||
"Add Panel": "Přidat panel",
|
"Add Panel": "Přidat panel",
|
||||||
"Add Field": "Přidat pole",
|
"Add Field": "Přidat pole",
|
||||||
"Settings": "Nastavení",
|
"Settings": "Nastavení",
|
||||||
"Scheduled Jobs": "Naplánované akce",
|
"Scheduled Jobs": "Naplánované akce",
|
||||||
"Upgrade": "Aktualizace",
|
"Upgrade": "Aktualizace",
|
||||||
"Clear Cache": "Vyčistit cache",
|
"Clear Cache": "Vyčistit cache",
|
||||||
"Rebuild": "Přestavět",
|
"Rebuild": "Přestavět",
|
||||||
"Teams": "Týmy",
|
"Teams": "Týmy",
|
||||||
"Roles": "Role",
|
"Roles": "Role",
|
||||||
"Portal": "Portál",
|
"Portal": "Portál",
|
||||||
"Portals": "Portály",
|
"Portals": "Portály",
|
||||||
"Portal Roles": "Role portálu",
|
"Portal Roles": "Role portálu",
|
||||||
"Outbound Emails": "Odchozí emaily",
|
"Outbound Emails": "Odchozí emaily",
|
||||||
"Group Email Accounts": "Skupinové e-mailové účty",
|
"Group Email Accounts": "Skupinové e-mailové účty",
|
||||||
"Personal Email Accounts": "Osobní e-mailové účty",
|
"Personal Email Accounts": "Osobní e-mailové účty",
|
||||||
"Inbound Emails": "Příchozí emaily",
|
"Inbound Emails": "Příchozí emaily",
|
||||||
"Email Templates": "Šablony emailů",
|
"Email Templates": "Šablony emailů",
|
||||||
"Layout Manager": "Správa layoutu",
|
"Layout Manager": "Správa layoutu",
|
||||||
"User Interface": "Uživatelské rozhraní",
|
"User Interface": "Uživatelské rozhraní",
|
||||||
"Auth Tokens": "Autentizační tokeny",
|
"Auth Tokens": "Autentizační tokeny",
|
||||||
"Authentication": "Autentizace",
|
"Authentication": "Autentizace",
|
||||||
"Currency": "Měna",
|
"Currency": "Měna",
|
||||||
"Integrations": "Integrace",
|
"Integrations": "Integrace",
|
||||||
"Extensions": "Rozšíření",
|
"Extensions": "Rozšíření",
|
||||||
"Upload": "Nahrát",
|
"Upload": "Nahrát",
|
||||||
"Installing...": "Instaluji...",
|
"Installing...": "Instaluji...",
|
||||||
"Upgrading...": "Upgraduji...",
|
"Upgrading...": "Upgraduji...",
|
||||||
"Upgraded successfully": "Úspěšně upgradováno",
|
"Upgraded successfully": "Úspěšně upgradováno",
|
||||||
"Installed successfully": "Úspěšně nainstalováno",
|
"Installed successfully": "Úspěšně nainstalováno",
|
||||||
"Ready for upgrade": "Připraveno k upgradu",
|
"Ready for upgrade": "Připraveno k upgradu",
|
||||||
"Run Upgrade": "Spustit upgrade",
|
"Run Upgrade": "Spustit upgrade",
|
||||||
"Install": "Instalovat",
|
"Install": "Instalovat",
|
||||||
"Ready for installation": "Připraveno k instalaci",
|
"Ready for installation": "Připraveno k instalaci",
|
||||||
"Uninstalling...": "Odebírám...",
|
"Uninstalling...": "Odebírám...",
|
||||||
"Uninstalled": "Odebráno",
|
"Uninstalled": "Odebráno",
|
||||||
"Create Entity": "Vytvořit entitu",
|
"Create Entity": "Vytvořit entitu",
|
||||||
"Edit Entity": "Upravit entitu",
|
"Edit Entity": "Upravit entitu",
|
||||||
"Create Link": "Vytvořit vazbu",
|
"Create Link": "Vytvořit vazbu",
|
||||||
"Edit Link": "Upravit link",
|
"Edit Link": "Upravit link",
|
||||||
"Notifications": "Upozornění",
|
"Notifications": "Upozornění",
|
||||||
"Jobs": "Joby",
|
"Jobs": "Joby",
|
||||||
"Reset to Default": "Obnovit do základního nastavení",
|
"Reset to Default": "Obnovit do základního nastavení",
|
||||||
"Email Filters": "E-mailové filtry",
|
"Email Filters": "E-mailové filtry",
|
||||||
"Portal Users": "Uživatelé portálu",
|
"Portal Users": "Uživatelé portálu",
|
||||||
"Action History": "Historie akcí",
|
"Action History": "Historie akcí",
|
||||||
"Label Manager": "Správce labelů",
|
"Label Manager": "Správce labelů",
|
||||||
"Auth Log": "Log autentizace",
|
"Auth Log": "Log autentizace",
|
||||||
"Lead Capture": "Zachycení potenciálů",
|
"Lead Capture": "Zachycení potenciálů",
|
||||||
"Attachments": "Přílohy",
|
"Attachments": "Přílohy",
|
||||||
"API Users": "API uživatelé",
|
"API Users": "API uživatelé",
|
||||||
"Template Manager": "Správce šablon",
|
"Template Manager": "Správce šablon",
|
||||||
"System Requirements": "Požadavky na systém",
|
"System Requirements": "Požadavky na systém",
|
||||||
"PHP Settings": "Nastavení PHP",
|
"PHP Settings": "Nastavení PHP",
|
||||||
"Database Settings": "Nastavení databáze",
|
"Database Settings": "Nastavení databáze",
|
||||||
"Permissions": "Oprávnění",
|
"Permissions": "Oprávnění",
|
||||||
"Success": "Úspěch",
|
"Success": "Úspěch",
|
||||||
"Fail": "Selhání",
|
"Fail": "Selhání",
|
||||||
"is recommended": "je doporučeno",
|
"is recommended": "je doporučeno",
|
||||||
"extension is missing": "rozšíření chybí",
|
"extension is missing": "rozšíření chybí",
|
||||||
"PDF Templates": "PDF Šablony",
|
"PDF Templates": "PDF Šablony",
|
||||||
"Webhooks": "Webhooky",
|
"Webhooks": "Webhooky",
|
||||||
"Dashboard Templates": "Šablony hlavních panelů",
|
"Dashboard Templates": "Šablony hlavních panelů",
|
||||||
"Email Addresses": "Emailové adresy",
|
"Email Addresses": "Emailové adresy",
|
||||||
"Phone Numbers": "Telefonní čísla",
|
"Phone Numbers": "Telefonní čísla",
|
||||||
"Layout Sets": "Sady vzhledů",
|
"Layout Sets": "Sady vzhledů",
|
||||||
"Messaging": "Zprávy",
|
"Messaging": "Zprávy",
|
||||||
"Misc": "Vedlejší",
|
"Misc": "Vedlejší",
|
||||||
"Job Settings": "Nastavení jobů",
|
"Job Settings": "Nastavení jobů",
|
||||||
"Configuration Instructions": "Instrukce k nastavení",
|
"Configuration Instructions": "Instrukce k nastavení",
|
||||||
"Formula Sandbox": "Pískoviště pro formula skripty",
|
"Formula Sandbox": "Pískoviště pro formula skripty",
|
||||||
"Working Time Calendars": "Kalendáře pracovní doby",
|
"Working Time Calendars": "Kalendáře pracovní doby",
|
||||||
"Group Email Folders": "Složky skupinových e-mailů",
|
"Group Email Folders": "Složky skupinových e-mailů",
|
||||||
"Authentication Providers": "Poskytovatelé autentizace",
|
"Authentication Providers": "Poskytovatelé autentizace",
|
||||||
"Setup": "Nastavení",
|
"Setup": "Nastavení",
|
||||||
"App Log": "Log aplikace",
|
"App Log": "Log aplikace",
|
||||||
"Address Countries": "Seznam zemí",
|
"Address Countries": "Seznam zemí",
|
||||||
"App Secrets": "Tajemství aplikace",
|
"App Secrets": "Tajemství aplikace",
|
||||||
"OAuth Providers": "OAuth poskytovatelé"
|
"OAuth Providers": "OAuth poskytovatelé"
|
||||||
},
|
},
|
||||||
"layouts": {
|
"layouts": {
|
||||||
"list": "Seznam",
|
"list": "Seznam",
|
||||||
"listSmall": "Seznam (malý)",
|
"listSmall": "Seznam (malý)",
|
||||||
"detailSmall": "Detail (malý)",
|
"detailSmall": "Detail (malý)",
|
||||||
"filters": "Vyhledávací filtry",
|
"filters": "Vyhledávací filtry",
|
||||||
"massUpdate": "Hromadný update",
|
"massUpdate": "Hromadný update",
|
||||||
"relationships": "Vztah",
|
"relationships": "Vztah",
|
||||||
"sidePanelsDetail": "Boční panely (Detail)",
|
"sidePanelsDetail": "Boční panely (Detail)",
|
||||||
"sidePanelsEdit": "Boční panely (Upravit)",
|
"sidePanelsEdit": "Boční panely (Upravit)",
|
||||||
"sidePanelsDetailSmall": "Boční panely (Detail malé)",
|
"sidePanelsDetailSmall": "Boční panely (Detail malé)",
|
||||||
"sidePanelsEditSmall": "Boční panely (Upravit malé)",
|
"sidePanelsEditSmall": "Boční panely (Upravit malé)",
|
||||||
"detailPortal": "Detail (Portál)",
|
"detailPortal": "Detail (Portál)",
|
||||||
"detailSmallPortal": "Detail (Small, Portál)",
|
"detailSmallPortal": "Detail (Small, Portál)",
|
||||||
"listSmallPortal": "Seznam malý (Portál)",
|
"listSmallPortal": "Seznam malý (Portál)",
|
||||||
"listPortal": "Seznam (portál)",
|
"listPortal": "Seznam (portál)",
|
||||||
"relationshipsPortal": "Panely vztahů (Portál)",
|
"relationshipsPortal": "Panely vztahů (Portál)",
|
||||||
"defaultSidePanel": "Pole bočního panelu",
|
"defaultSidePanel": "Pole bočního panelu",
|
||||||
"bottomPanelsDetail": "Spodní panely",
|
"bottomPanelsDetail": "Spodní panely",
|
||||||
"bottomPanelsEdit": "Spodní panely (Upravit)",
|
"bottomPanelsEdit": "Spodní panely (Upravit)",
|
||||||
"bottomPanelsDetailSmall": "Spodní panely (Detail malé)",
|
"bottomPanelsDetailSmall": "Spodní panely (Detail malé)",
|
||||||
"bottomPanelsEditSmall": "Spodní panely (Upravit malé)"
|
"bottomPanelsEditSmall": "Spodní panely (Upravit malé)"
|
||||||
},
|
},
|
||||||
"fieldTypes": {
|
"fieldTypes": {
|
||||||
"address": "Adresa",
|
"address": "Adresa",
|
||||||
"array": "Pole",
|
"array": "Pole",
|
||||||
"foreign": "Cizí pole",
|
"foreign": "Cizí pole",
|
||||||
"duration": "Trvání",
|
"duration": "Trvání",
|
||||||
"password": "Heslo",
|
"password": "Heslo",
|
||||||
"personName": "Jméno osoby",
|
"personName": "Jméno osoby",
|
||||||
"autoincrement": "Číslo (automaticky zvyšované)",
|
"autoincrement": "Číslo (automaticky zvyšované)",
|
||||||
"bool": "Ano/Ne",
|
"bool": "Ano/Ne",
|
||||||
"currency": "Měna",
|
"currency": "Měna",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
"enum": "Výběr",
|
"enum": "Výběr",
|
||||||
"enumInt": "Výběr (číslo)",
|
"enumInt": "Výběr (číslo)",
|
||||||
"enumFloat": "Výběr (desetinné číslo)",
|
"enumFloat": "Výběr (desetinné číslo)",
|
||||||
"float": "Číslo (desetinné)",
|
"float": "Číslo (desetinné)",
|
||||||
"link": "Vazba",
|
"link": "Vazba",
|
||||||
"linkMultiple": "Vazba (vícenásobná)",
|
"linkMultiple": "Vazba (vícenásobná)",
|
||||||
"linkParent": "Vazba (rodič)",
|
"linkParent": "Vazba (rodič)",
|
||||||
"phone": "Telefon",
|
"phone": "Telefon",
|
||||||
"url": "URL adresa",
|
"url": "URL adresa",
|
||||||
"file": "Soubor",
|
"file": "Soubor",
|
||||||
"image": "Obrázek",
|
"image": "Obrázek",
|
||||||
"multiEnum": "Výběr (vícenásobný)",
|
"multiEnum": "Výběr (vícenásobný)",
|
||||||
"attachmentMultiple": "Více příloh",
|
"attachmentMultiple": "Více příloh",
|
||||||
"rangeInt": "Rozsah (celé číslo)",
|
"rangeInt": "Rozsah (celé číslo)",
|
||||||
"rangeFloat": "Rozsah (desetinné číslo)",
|
"rangeFloat": "Rozsah (desetinné číslo)",
|
||||||
"rangeCurrency": "Rozsah (měna)",
|
"rangeCurrency": "Rozsah (měna)",
|
||||||
"wysiwyg": "WYSIWYG editor",
|
"wysiwyg": "WYSIWYG editor",
|
||||||
"map": "Mapa",
|
"map": "Mapa",
|
||||||
"currencyConverted": "Měna (převedená)",
|
"currencyConverted": "Měna (převedená)",
|
||||||
"colorpicker": "Výběr barvy",
|
"colorpicker": "Výběr barvy",
|
||||||
"int": "Číslo (celé)",
|
"int": "Číslo (celé)",
|
||||||
"number": "Číslo faktury",
|
"number": "Číslo faktury",
|
||||||
"jsonArray": "JSON pole",
|
"jsonArray": "JSON pole",
|
||||||
"jsonObject": "JSON objekt",
|
"jsonObject": "JSON objekt",
|
||||||
"datetime": "Datum a čas",
|
"datetime": "Datum a čas",
|
||||||
"datetimeOptional": "Datum/Datum a čas",
|
"datetimeOptional": "Datum/Datum a čas",
|
||||||
"checklist": "Ano/Ne (seznam)",
|
"checklist": "Ano/Ne (seznam)",
|
||||||
"linkOne": "Vazba (jednonásobná)",
|
"linkOne": "Vazba (jednonásobná)",
|
||||||
"barcode": "Čárový kód",
|
"barcode": "Čárový kód",
|
||||||
"urlMultiple": "URL adresy (více)",
|
"urlMultiple": "URL adresy (více)",
|
||||||
"base": "Výchozí",
|
"base": "Výchozí",
|
||||||
"decimal": "Desetinné číslo"
|
"decimal": "Desetinné číslo"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
"name": "Jméno",
|
"name": "Jméno",
|
||||||
"label": "Popisek",
|
"label": "Popisek",
|
||||||
"required": "Povinné",
|
"required": "Povinné",
|
||||||
"default": "Výchozí",
|
"default": "Výchozí",
|
||||||
"maxLength": "Maximální délka",
|
"maxLength": "Maximální délka",
|
||||||
"options": "Možnosti",
|
"options": "Možnosti",
|
||||||
"after": "Po (pole)",
|
"after": "Po (pole)",
|
||||||
"before": "Před (pole)",
|
"before": "Před (pole)",
|
||||||
"link": "Odkaz",
|
"link": "Odkaz",
|
||||||
"field": "Pole",
|
"field": "Pole",
|
||||||
"min": "Minimum",
|
"min": "Minimum",
|
||||||
"max": "Maximum",
|
"max": "Maximum",
|
||||||
"translation": "Překlad",
|
"translation": "Překlad",
|
||||||
"previewSize": "Velikost náhledu",
|
"previewSize": "Velikost náhledu",
|
||||||
"defaultType": "Výchozí typ",
|
"defaultType": "Výchozí typ",
|
||||||
"seeMoreDisabled": "Zakázat ořez textu",
|
"seeMoreDisabled": "Zakázat ořez textu",
|
||||||
"entityList": "Seznam entit",
|
"entityList": "Seznam entit",
|
||||||
"isSorted": "Je seřazeno (abecedně)",
|
"isSorted": "Je seřazeno (abecedně)",
|
||||||
"audited": "Auditováno",
|
"audited": "Auditováno",
|
||||||
"trim": "Oříznout",
|
"trim": "Oříznout",
|
||||||
"height": "Výška (px)",
|
"height": "Výška (px)",
|
||||||
"minHeight": "Minimální výška (px)",
|
"minHeight": "Minimální výška (px)",
|
||||||
"provider": "Poskytovatel",
|
"provider": "Poskytovatel",
|
||||||
"typeList": "Seznam typů",
|
"typeList": "Seznam typů",
|
||||||
"lengthOfCut": "Délka řezu",
|
"lengthOfCut": "Délka řezu",
|
||||||
"sourceList": "Seznam zdrojů",
|
"sourceList": "Seznam zdrojů",
|
||||||
"tooltipText": "Text nápovědy",
|
"tooltipText": "Text nápovědy",
|
||||||
"prefix": "Předpona",
|
"prefix": "Předpona",
|
||||||
"nextNumber": "Další číslo",
|
"nextNumber": "Další číslo",
|
||||||
"padLength": "Délka výplně",
|
"padLength": "Délka výplně",
|
||||||
"disableFormatting": "Zakázat formátování",
|
"disableFormatting": "Zakázat formátování",
|
||||||
"dynamicLogicVisible": "Podmínky, za kterých je pole viditelné",
|
"dynamicLogicVisible": "Podmínky, za kterých je pole viditelné",
|
||||||
"dynamicLogicReadOnly": "Podmínky, za kterých je pole jenom pro čtení",
|
"dynamicLogicReadOnly": "Podmínky, za kterých je pole jenom pro čtení",
|
||||||
"dynamicLogicRequired": "Podmínky, za kterých je pole povinné",
|
"dynamicLogicRequired": "Podmínky, za kterých je pole povinné",
|
||||||
"dynamicLogicOptions": "Podmíněné možnosti",
|
"dynamicLogicOptions": "Podmíněné možnosti",
|
||||||
"probabilityMap": "Pravděpodobnosti fáze (%)",
|
"probabilityMap": "Pravděpodobnosti fáze (%)",
|
||||||
"readOnly": "Pouze ke čtení",
|
"readOnly": "Pouze ke čtení",
|
||||||
"noEmptyString": "Neprázdný řetězec",
|
"noEmptyString": "Neprázdný řetězec",
|
||||||
"maxFileSize": "Maximální velikost souboru (Mb)",
|
"maxFileSize": "Maximální velikost souboru (Mb)",
|
||||||
"isPersonalData": "Jsou osobní údaje",
|
"isPersonalData": "Jsou osobní údaje",
|
||||||
"useIframe": "Použít iframe",
|
"useIframe": "Použít iframe",
|
||||||
"useNumericFormat": "Použít číselný formát",
|
"useNumericFormat": "Použít číselný formát",
|
||||||
"strip": "Odstranit",
|
"strip": "Odstranit",
|
||||||
"cutHeight": "Oříznout výšku (px)",
|
"cutHeight": "Oříznout výšku (px)",
|
||||||
"minuteStep": "Minutový krok",
|
"minuteStep": "Minutový krok",
|
||||||
"inlineEditDisabled": "Zakázat samostatnou úpravu",
|
"inlineEditDisabled": "Zakázat samostatnou úpravu",
|
||||||
"displayAsLabel": "Zobrazit jako štítek",
|
"displayAsLabel": "Zobrazit jako štítek",
|
||||||
"allowCustomOptions": "Povolit vlastní možnosti",
|
"allowCustomOptions": "Povolit vlastní možnosti",
|
||||||
"maxCount": "Maximální počet položek",
|
"maxCount": "Maximální počet položek",
|
||||||
"displayRawText": "Zobrazit holý text (bez označení)",
|
"displayRawText": "Zobrazit holý text (bez označení)",
|
||||||
"notActualOptions": "Neopravdové možnosti",
|
"notActualOptions": "Neopravdové možnosti",
|
||||||
"accept": "Přijmout",
|
"accept": "Přijmout",
|
||||||
"displayAsList": "Zobrazit jako seznam",
|
"displayAsList": "Zobrazit jako seznam",
|
||||||
"viewMap": "Zobrazit mapu",
|
"viewMap": "Zobrazit mapu",
|
||||||
"codeType": "Typ kódu",
|
"codeType": "Typ kódu",
|
||||||
"lastChar": "Poslední znak",
|
"lastChar": "Poslední znak",
|
||||||
"listPreviewSize": "Velikost náhledu seznamu",
|
"listPreviewSize": "Velikost náhledu seznamu",
|
||||||
"onlyDefaultCurrency": "Pouze výchozí měna",
|
"onlyDefaultCurrency": "Pouze výchozí měna",
|
||||||
"dynamicLogicInvalid": "Podmínky, které pole dělají neplatným",
|
"dynamicLogicInvalid": "Podmínky, které pole dělají neplatným",
|
||||||
"conversionDisabled": "Konverze zakázána",
|
"conversionDisabled": "Konverze zakázána",
|
||||||
"decimalPlaces": "Počet desetinných míst",
|
"decimalPlaces": "Počet desetinných míst",
|
||||||
"pattern": "Vzor",
|
"pattern": "Vzor",
|
||||||
"globalRestrictions": "Globální omezení",
|
"globalRestrictions": "Globální omezení",
|
||||||
"decimal": "Desetinné",
|
"decimal": "Desetinné",
|
||||||
"optionsReference": "Odkaz na možnosti",
|
"optionsReference": "Odkaz na možnosti",
|
||||||
"copyToClipboard": "Tlačítko na zkopírování do schránky",
|
"copyToClipboard": "Tlačítko na zkopírování do schránky",
|
||||||
"rows": "Počet řádků textové oblasti",
|
"rows": "Počet řádků textové oblasti",
|
||||||
"readOnlyAfterCreate": "Pouze ke čtení po vytvoření",
|
"readOnlyAfterCreate": "Pouze ke čtení po vytvoření",
|
||||||
"createButton": "Tlačítko pro vytváření",
|
"createButton": "Tlačítko pro vytváření",
|
||||||
"autocompleteOnEmpty": "Doplňování při prázdném poli",
|
"autocompleteOnEmpty": "Doplňování při prázdném poli",
|
||||||
"relateOnImport": "Provázat při importu",
|
"relateOnImport": "Provázat při importu",
|
||||||
"aclScope": "Entita pro acl",
|
"aclScope": "Entita pro acl",
|
||||||
"onlyAdmin": "Pouze pro administrátory",
|
"onlyAdmin": "Pouze pro administrátory",
|
||||||
"activeOptions": "Aktivní možnosti",
|
"activeOptions": "Aktivní možnosti",
|
||||||
"labelType": "Typ zobrazení",
|
"labelType": "Typ zobrazení",
|
||||||
"preview": "Náhled",
|
"preview": "Náhled",
|
||||||
"attachmentField": "Pole pro přílohu",
|
"attachmentField": "Pole pro přílohu",
|
||||||
"dynamicLogicReadOnlySaved": "Podmínky, za kterých je pole jenom pro čtení (po uložení)",
|
"dynamicLogicReadOnlySaved": "Podmínky, za kterých je pole jenom pro čtení (po uložení)",
|
||||||
"notStorable": "Neuložitelné",
|
"notStorable": "Neuložitelné",
|
||||||
"itemsEditable": "Upravitelné položky"
|
"itemsEditable": "Upravitelné položky"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"selectEntityType": "Vybrat entitu v levém menu.",
|
"selectEntityType": "Vybrat entitu v levém menu.",
|
||||||
"selectUpgradePackage": "Vybrat upgrade balíček",
|
"selectUpgradePackage": "Vybrat upgrade balíček",
|
||||||
"selectLayout": "Vybrat požadovaný layout v levém menu a upravit ho.",
|
"selectLayout": "Vybrat požadovaný layout v levém menu a upravit ho.",
|
||||||
"selectExtensionPackage": "Vybrat soubor s rozšířením",
|
"selectExtensionPackage": "Vybrat soubor s rozšířením",
|
||||||
"extensionInstalled": "Rozšíření {name} {version} bylo nainstalováno.",
|
"extensionInstalled": "Rozšíření {name} {version} bylo nainstalováno.",
|
||||||
"installExtension": "Rozšíření {name} {version} je připraveno k instalaci.",
|
"installExtension": "Rozšíření {name} {version} je připraveno k instalaci.",
|
||||||
"upgradeBackup": "Doporučujeme zálohovat soubory a data EspoCRM před upgradem.",
|
"upgradeBackup": "Doporučujeme zálohovat soubory a data EspoCRM před upgradem.",
|
||||||
"thousandSeparatorEqualsDecimalMark": "Oddělovač tisíců nemůže být stejný jako desetinný symbol.",
|
"thousandSeparatorEqualsDecimalMark": "Oddělovač tisíců nemůže být stejný jako desetinný symbol.",
|
||||||
"userHasNoEmailAddress": "Uživatel nemá emailovou adresu.",
|
"userHasNoEmailAddress": "Uživatel nemá emailovou adresu.",
|
||||||
"uninstallConfirmation": "Opravdu odinstalovat vybrané rozšíření?",
|
"uninstallConfirmation": "Opravdu odinstalovat vybrané rozšíření?",
|
||||||
"cronIsNotConfigured": "Naplánované úlohy nejsou spuštěny. Příchozí e-maily, oznámení a připomenutí proto nefungují. Postupujte podle [pokynů](https://www.espocrm.com/documentation/administration/server-configuration/#user-content-setup-a-crontab) k nastavení úlohy cron.",
|
"cronIsNotConfigured": "Naplánované úlohy nejsou spuštěny. Příchozí e-maily, oznámení a připomenutí proto nefungují. Postupujte podle [pokynů](https://www.espocrm.com/documentation/administration/server-configuration/#user-content-setup-a-crontab) k nastavení úlohy cron.",
|
||||||
"newExtensionVersionIsAvailable": "Je k dispozici nová verze {latestName} {latestVersion}.",
|
"newExtensionVersionIsAvailable": "Je k dispozici nová verze {latestName} {latestVersion}.",
|
||||||
"upgradeVersion": "EspoCRM bude upgradováno na verzi <strong>{version}</strong>. Toto může chvíli trvat.",
|
"upgradeVersion": "EspoCRM bude upgradováno na verzi <strong>{version}</strong>. Toto může chvíli trvat.",
|
||||||
"upgradeDone": "EspoCRM bylo upgradováno na verzi <strong>{version}</strong>.",
|
"upgradeDone": "EspoCRM bylo upgradováno na verzi <strong>{version}</strong>.",
|
||||||
"downloadUpgradePackage": "Stáhnout upgradovací balíčky na [tomto]({url}) odkaze.",
|
"downloadUpgradePackage": "Stáhnout upgradovací balíčky na [tomto]({url}) odkaze.",
|
||||||
"upgradeInfo": "Přečtěte si [dokumentaci]({url}) o tom, jak upgradovat instanci AutoCRM.",
|
"upgradeInfo": "Přečtěte si [dokumentaci]({url}) o tom, jak upgradovat instanci EspoCRM.",
|
||||||
"upgradeRecommendation": "Tento způsob upgradu se nedoporučuje. Je lepší upgradovat z CLI.",
|
"upgradeRecommendation": "Tento způsob upgradu se nedoporučuje. Je lepší upgradovat z CLI.",
|
||||||
"newVersionIsAvailable": "K dispozici je nová verze AutoCRM {latestVersion}. Při aktualizaci instance postupujte podle [pokynů](https://www.espocrm.com/documentation/administration/upgrading/).",
|
"newVersionIsAvailable": "K dispozici je nová verze EspoCRM {latestVersion}. Při aktualizaci instance postupujte podle [pokynů](https://www.espocrm.com/documentation/administration/upgrading/).",
|
||||||
"formulaFunctions": "Funkce formula skriptů",
|
"formulaFunctions": "Funkce formula skriptů",
|
||||||
"rebuildRequired": "Musíte spustit znovu rebuild z CLI.",
|
"rebuildRequired": "Musíte spustit znovu rebuild z CLI.",
|
||||||
"cronIsDisabled": "Cron je zakázán",
|
"cronIsDisabled": "Cron je zakázán",
|
||||||
"cacheIsDisabled": "Cache je zakázána"
|
"cacheIsDisabled": "Cache je zakázána"
|
||||||
},
|
},
|
||||||
"descriptions": {
|
"descriptions": {
|
||||||
"settings": "Systémová nastavení aplikace.",
|
"settings": "Systémová nastavení aplikace.",
|
||||||
"scheduledJob": "Činnosti vykonávané CRONem.",
|
"scheduledJob": "Činnosti vykonávané CRONem.",
|
||||||
"upgrade": "Upgradovat EspoCRM.",
|
"upgrade": "Upgradovat EspoCRM.",
|
||||||
"clearCache": "Vyčistit veškerou cache.",
|
"clearCache": "Vyčistit veškerou cache.",
|
||||||
"rebuild": "Přestavět backend a vyčistit cache.",
|
"rebuild": "Přestavět backend a vyčistit cache.",
|
||||||
"users": "Správa uživatelů.",
|
"users": "Správa uživatelů.",
|
||||||
"teams": "Správa týmů.",
|
"teams": "Správa týmů.",
|
||||||
"roles": "Správa rolí.",
|
"roles": "Správa rolí.",
|
||||||
"portals": "Správa portálů.",
|
"portals": "Správa portálů.",
|
||||||
"portalRoles": "Role pro portál.",
|
"portalRoles": "Role pro portál.",
|
||||||
"outboundEmails": "Nastavení SMTP pro odchozí emaily.",
|
"outboundEmails": "Nastavení SMTP pro odchozí emaily.",
|
||||||
"groupEmailAccounts": "Skupinové IMAP emailové účty. Import emailů",
|
"groupEmailAccounts": "Skupinové IMAP emailové účty. Import emailů",
|
||||||
"personalEmailAccounts": "E-mailové účty uživatelů.",
|
"personalEmailAccounts": "E-mailové účty uživatelů.",
|
||||||
"emailTemplates": "Šablony pro odchozí emaily.",
|
"emailTemplates": "Šablony pro odchozí emaily.",
|
||||||
"import": "Importovat data z CSV souboru.",
|
"import": "Importovat data z CSV souboru.",
|
||||||
"layoutManager": "Přizpůsobit layouty (seznam, detail, upravit, hledat, hromadný update).",
|
"layoutManager": "Přizpůsobit layouty (seznam, detail, upravit, hledat, hromadný update).",
|
||||||
"userInterface": "Nastavit uživatelské rozhraní.",
|
"userInterface": "Nastavit uživatelské rozhraní.",
|
||||||
"authTokens": "Aktivní autentizační sessions. IP adresa a datum posledního přístupu.",
|
"authTokens": "Aktivní autentizační sessions. IP adresa a datum posledního přístupu.",
|
||||||
"authentication": "Nastavení autentizace.",
|
"authentication": "Nastavení autentizace.",
|
||||||
"currency": "Nastavení měn a kurzů.",
|
"currency": "Nastavení měn a kurzů.",
|
||||||
"extensions": "Instalovat a odebrat rozšíření.",
|
"extensions": "Instalovat a odebrat rozšíření.",
|
||||||
"integrations": "Integrace se službami třetích stran.",
|
"integrations": "Integrace se službami třetích stran.",
|
||||||
"notifications": "Nastavení In-app a emailových upozornění.",
|
"notifications": "Nastavení In-app a emailových upozornění.",
|
||||||
"inboundEmails": "Nastavení příchozích mailů",
|
"inboundEmails": "Nastavení příchozích mailů",
|
||||||
"portalUsers": "Uživatelé portálu.",
|
"portalUsers": "Uživatelé portálu.",
|
||||||
"entityManager": "Vytvořit vlastní entity, úpravit existující. Správa polí a vztahů.",
|
"entityManager": "Vytvořit vlastní entity, úpravit existující. Správa polí a vztahů.",
|
||||||
"emailFilters": "E-mailové zprávy, které odpovídají zadanému filtru, nebudou importovány.",
|
"emailFilters": "E-mailové zprávy, které odpovídají zadanému filtru, nebudou importovány.",
|
||||||
"actionHistory": "Protokol akcí uživatelů.",
|
"actionHistory": "Protokol akcí uživatelů.",
|
||||||
"labelManager": "Upravit popisky",
|
"labelManager": "Upravit popisky",
|
||||||
"authLog": "Historie přihlášení.",
|
"authLog": "Historie přihlášení.",
|
||||||
"attachments": "Všechny přílohy souborů uložené v systému.",
|
"attachments": "Všechny přílohy souborů uložené v systému.",
|
||||||
"templateManager": "Přizpůsobte si šablony zpráv.",
|
"templateManager": "Přizpůsobte si šablony zpráv.",
|
||||||
"systemRequirements": "Systémové požadavky na AutoCRM.",
|
"systemRequirements": "Systémové požadavky na EspoCRM.",
|
||||||
"apiUsers": "Oddělte uživatele pro účely integrace.",
|
"apiUsers": "Oddělte uživatele pro účely integrace.",
|
||||||
"jobs": "Spustit akce na pozadí.",
|
"jobs": "Spustit akce na pozadí.",
|
||||||
"pdfTemplates": "Šablony pro tisk do PDF.",
|
"pdfTemplates": "Šablony pro tisk do PDF.",
|
||||||
"webhooks": "Správa webhooků.",
|
"webhooks": "Správa webhooků.",
|
||||||
"dashboardTemplates": "Umožňuje přidávat dashboardy uživatelům.",
|
"dashboardTemplates": "Umožňuje přidávat dashboardy uživatelům.",
|
||||||
"phoneNumbers": "Všechna telefonní čísla uložená v systému.",
|
"phoneNumbers": "Všechna telefonní čísla uložená v systému.",
|
||||||
"emailAddresses": "Všechny e-mailové adresy uložené v systému.",
|
"emailAddresses": "Všechny e-mailové adresy uložené v systému.",
|
||||||
"layoutSets": "Kolekce layoutů, které lze přiřadit týmům a portálům.",
|
"layoutSets": "Kolekce layoutů, které lze přiřadit týmům a portálům.",
|
||||||
"jobsSettings": "Nastavení zpracování jobů. Joby vykonávají úkoly na pozadí.",
|
"jobsSettings": "Nastavení zpracování jobů. Joby vykonávají úkoly na pozadí.",
|
||||||
"sms": "Nastavení SMS.",
|
"sms": "Nastavení SMS.",
|
||||||
"formulaSandbox": "Pískoviště pro testování formula skriptů bez ukládání změn.",
|
"formulaSandbox": "Pískoviště pro testování formula skriptů bez ukládání změn.",
|
||||||
"workingTimeCalendars": "Pracovní plány.",
|
"workingTimeCalendars": "Pracovní plány.",
|
||||||
"groupEmailFolders": "Složky sdílené pro týmy",
|
"groupEmailFolders": "Složky sdílené pro týmy",
|
||||||
"authenticationProviders": "Další poskytovatelé autentizace pro portály.",
|
"authenticationProviders": "Další poskytovatelé autentizace pro portály.",
|
||||||
"appLog": "Log aplikace.",
|
"appLog": "Log aplikace.",
|
||||||
"addressCountries": "Dostupné země pro políčka typu 'adresa'.",
|
"addressCountries": "Dostupné země pro políčka typu 'adresa'.",
|
||||||
"appSecrets": "Pro ukládání citlivých informací jako jsou API klíče, hesla, a jiná tajemství.",
|
"appSecrets": "Pro ukládání citlivých informací jako jsou API klíče, hesla, a jiná tajemství.",
|
||||||
"leadCapture": "Koncové body pro zachycení potenciálů a webové formuláře.",
|
"leadCapture": "Koncové body pro zachycení potenciálů a webové formuláře.",
|
||||||
"oAuthProviders": "OAuth poskytovatelé pro integrace."
|
"oAuthProviders": "OAuth poskytovatelé pro integrace."
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"previewSize": {
|
"previewSize": {
|
||||||
"x-small": "Extra-malý",
|
"x-small": "Extra-malý",
|
||||||
"small": "Malý",
|
"small": "Malý",
|
||||||
"medium": "Střední",
|
"medium": "Střední",
|
||||||
"large": "Velký",
|
"large": "Velký",
|
||||||
"": "Prázdné"
|
"": "Prázdné"
|
||||||
},
|
},
|
||||||
"labelType": {
|
"labelType": {
|
||||||
"state": "Stav",
|
"state": "Stav",
|
||||||
"regular": "Výchozí"
|
"regular": "Výchozí"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"logicalOperators": {
|
"logicalOperators": {
|
||||||
"and": "a zároveň",
|
"and": "a zároveň",
|
||||||
"or": "nebo",
|
"or": "nebo",
|
||||||
"not": "negace"
|
"not": "negace"
|
||||||
},
|
},
|
||||||
"systemRequirements": {
|
"systemRequirements": {
|
||||||
"requiredPhpVersion": "Požadovaná verze PHP",
|
"requiredPhpVersion": "Požadovaná verze PHP",
|
||||||
"requiredMysqlVersion": "Požadovaná verze MySQL",
|
"requiredMysqlVersion": "Požadovaná verze MySQL",
|
||||||
"host": "Jméno hostitele",
|
"host": "Jméno hostitele",
|
||||||
"dbname": "Název databáze",
|
"dbname": "Název databáze",
|
||||||
"user": "Uživatel",
|
"user": "Uživatel",
|
||||||
"writable": "Zapisovatelné",
|
"writable": "Zapisovatelné",
|
||||||
"readable": "Čitelné",
|
"readable": "Čitelné",
|
||||||
"requiredMariadbVersion": "Požadovaná verze MariaDB",
|
"requiredMariadbVersion": "Požadovaná verze MariaDB",
|
||||||
"requiredPostgresqlVersion": "Požadovaná verze PostgreSQL"
|
"requiredPostgresqlVersion": "Požadovaná verze PostgreSQL"
|
||||||
},
|
},
|
||||||
"templates": {
|
"templates": {
|
||||||
"accessInfo": "Přístupové údaje",
|
"accessInfo": "Přístupové údaje",
|
||||||
"accessInfoPortal": "Přístupové údaje na portály",
|
"accessInfoPortal": "Přístupové údaje na portály",
|
||||||
"assignment": "Úkol",
|
"assignment": "Úkol",
|
||||||
"mention": "Zmínka",
|
"mention": "Zmínka",
|
||||||
"notePost": "Poznámka k příspěvku",
|
"notePost": "Poznámka k příspěvku",
|
||||||
"notePostNoParent": "Poznámka k příspěvku (bez rodiče)",
|
"notePostNoParent": "Poznámka k příspěvku (bez rodiče)",
|
||||||
"noteStatus": "Poznámka k aktualizaci stavu",
|
"noteStatus": "Poznámka k aktualizaci stavu",
|
||||||
"passwordChangeLink": "Odkaz na změnu hesla",
|
"passwordChangeLink": "Odkaz na změnu hesla",
|
||||||
"noteEmailReceived": "Poznámka o přijatém e-mailu",
|
"noteEmailReceived": "Poznámka o přijatém e-mailu",
|
||||||
"twoFactorCode": "Dvoufaktorový kód"
|
"twoFactorCode": "Dvoufaktorový kód"
|
||||||
},
|
},
|
||||||
"strings": {
|
"strings": {
|
||||||
"rebuildRequired": "Rebuild je vyžadován."
|
"rebuildRequired": "Rebuild je vyžadován."
|
||||||
},
|
},
|
||||||
"keywords": {
|
"keywords": {
|
||||||
"settings": "nastavení",
|
"settings": "nastavení",
|
||||||
"userInterface": "uživatelské rozhraní",
|
"userInterface": "uživatelské rozhraní",
|
||||||
"scheduledJob": "naplánovaná akce",
|
"scheduledJob": "naplánovaná akce",
|
||||||
"integrations": "integrace",
|
"integrations": "integrace",
|
||||||
"authLog": "log autentizace",
|
"authLog": "log autentizace",
|
||||||
"authTokens": "autentizační tokeny",
|
"authTokens": "autentizační tokeny",
|
||||||
"entityManager": "správce entit",
|
"entityManager": "správce entit",
|
||||||
"templateManager": "správce šablon",
|
"templateManager": "správce šablon",
|
||||||
"jobs": "úlohy",
|
"jobs": "úlohy",
|
||||||
"authentication": "autentizace",
|
"authentication": "autentizace",
|
||||||
"labelManager": "správce popisků",
|
"labelManager": "správce popisků",
|
||||||
"appSecrets": "tajemství aplikace",
|
"appSecrets": "tajemství aplikace",
|
||||||
"leadCapture": "zachycení potenciálů"
|
"leadCapture": "zachycení potenciálů"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"tabUrl": "URL záložky",
|
"tabUrl": "URL záložky",
|
||||||
"tabUrlAclScope": "ACL rozsah pro záložku URL"
|
"tabUrlAclScope": "ACL rozsah pro záložku URL"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,313 +1,313 @@
|
|||||||
{
|
{
|
||||||
"fields": {
|
"fields": {
|
||||||
"useCache": "Použít cache",
|
"useCache": "Použít cache",
|
||||||
"dateFormat": "Formát data",
|
"dateFormat": "Formát data",
|
||||||
"timeFormat": "Formát času",
|
"timeFormat": "Formát času",
|
||||||
"timeZone": "Časové pásmo",
|
"timeZone": "Časové pásmo",
|
||||||
"weekStart": "První den v týdnu",
|
"weekStart": "První den v týdnu",
|
||||||
"thousandSeparator": "Oddělovač tisíců",
|
"thousandSeparator": "Oddělovač tisíců",
|
||||||
"decimalMark": "Desetinný oddělovač",
|
"decimalMark": "Desetinný oddělovač",
|
||||||
"defaultCurrency": "Výchozí měna",
|
"defaultCurrency": "Výchozí měna",
|
||||||
"baseCurrency": "Bázová měna",
|
"baseCurrency": "Bázová měna",
|
||||||
"currencyRates": "Kurzy měn",
|
"currencyRates": "Kurzy měn",
|
||||||
"currencyList": "Seznam měn",
|
"currencyList": "Seznam měn",
|
||||||
"language": "Jazyk",
|
"language": "Jazyk",
|
||||||
"companyLogo": "Logo společnosti",
|
"companyLogo": "Logo společnosti",
|
||||||
"ldapPort": "LDAP Port",
|
"ldapPort": "LDAP Port",
|
||||||
"ldapAuth": "LDAP Auth",
|
"ldapAuth": "LDAP Auth",
|
||||||
"ldapSecurity": "Zabezpečení",
|
"ldapSecurity": "Zabezpečení",
|
||||||
"ldapPassword": "Heslo",
|
"ldapPassword": "Heslo",
|
||||||
"outboundEmailFromName": "Od (jméno)",
|
"outboundEmailFromName": "Od (jméno)",
|
||||||
"outboundEmailIsShared": "Sdílení",
|
"outboundEmailIsShared": "Sdílení",
|
||||||
"recordsPerPage": "Záznamy na stránku",
|
"recordsPerPage": "Záznamy na stránku",
|
||||||
"recordsPerPageSmall": "Záznamy na stránku (malý)",
|
"recordsPerPageSmall": "Záznamy na stránku (malý)",
|
||||||
"tabList": "Seznam záložek",
|
"tabList": "Seznam záložek",
|
||||||
"quickCreateList": "Rychlé odkazy",
|
"quickCreateList": "Rychlé odkazy",
|
||||||
"exportDelimiter": "Export oddělovač",
|
"exportDelimiter": "Export oddělovač",
|
||||||
"globalSearchEntityList": "Seznam entit globálního vyhledávání",
|
"globalSearchEntityList": "Seznam entit globálního vyhledávání",
|
||||||
"authenticationMethod": "Autentizační metoda",
|
"authenticationMethod": "Autentizační metoda",
|
||||||
"ldapHost": "LDAP Host",
|
"ldapHost": "LDAP Host",
|
||||||
"ldapAccountCanonicalForm": "LDAP Account Canonical Form",
|
"ldapAccountCanonicalForm": "LDAP Account Canonical Form",
|
||||||
"ldapAccountDomainName": "Název domény účtu",
|
"ldapAccountDomainName": "Název domény účtu",
|
||||||
"ldapTryUsernameSplit": "Zkuste rozdělit uživatelské jméno",
|
"ldapTryUsernameSplit": "Zkuste rozdělit uživatelské jméno",
|
||||||
"ldapCreateEspoUser": "Vytvořit uživatele v EspoCRM",
|
"ldapCreateEspoUser": "Vytvořit uživatele v EspoCRM",
|
||||||
"ldapUserLoginFilter": "Filtr uživatelského přihlášení",
|
"ldapUserLoginFilter": "Filtr uživatelského přihlášení",
|
||||||
"ldapAccountDomainNameShort": "Account Domain Name krátké",
|
"ldapAccountDomainNameShort": "Account Domain Name krátké",
|
||||||
"ldapOptReferrals": "Volit doporučení",
|
"ldapOptReferrals": "Volit doporučení",
|
||||||
"exportDisabled": "Zakázat export (povolen pouze správce)",
|
"exportDisabled": "Zakázat export (povolen pouze správce)",
|
||||||
"b2cMode": "Režm B2C",
|
"b2cMode": "Režm B2C",
|
||||||
"avatarsDisabled": "Zakázat avatary",
|
"avatarsDisabled": "Zakázat avatary",
|
||||||
"displayListViewRecordCount": "Zobrazit celkový počet (v zobrazení seznamu)",
|
"displayListViewRecordCount": "Zobrazit celkový počet (v zobrazení seznamu)",
|
||||||
"theme": "Téma",
|
"theme": "Téma",
|
||||||
"userThemesDisabled": "Zakázat uživatelské motivy",
|
"userThemesDisabled": "Zakázat uživatelské motivy",
|
||||||
"emailMessageMaxSize": "Maximální velikost emailu (Mb)",
|
"emailMessageMaxSize": "Maximální velikost emailu (Mb)",
|
||||||
"personalEmailMaxPortionSize": "Maximální velikost emailové části pro načítání osobních účtů",
|
"personalEmailMaxPortionSize": "Maximální velikost emailové části pro načítání osobních účtů",
|
||||||
"inboundEmailMaxPortionSize": "Maximální velikost emailové části pro načítání skupinových účtů",
|
"inboundEmailMaxPortionSize": "Maximální velikost emailové části pro načítání skupinových účtů",
|
||||||
"authTokenLifetime": "Životnost ověřovacího tokenu (hodiny)",
|
"authTokenLifetime": "Životnost ověřovacího tokenu (hodiny)",
|
||||||
"authTokenMaxIdleTime": "Maximální doba nečinnosti ověřovacího tokenu (hodiny)",
|
"authTokenMaxIdleTime": "Maximální doba nečinnosti ověřovacího tokenu (hodiny)",
|
||||||
"dashboardLayout": "Rozvržení Dashboardu (výchozí)",
|
"dashboardLayout": "Rozvržení Dashboardu (výchozí)",
|
||||||
"siteUrl": "URL stránky",
|
"siteUrl": "URL stránky",
|
||||||
"addressPreview": "Náhled adresy",
|
"addressPreview": "Náhled adresy",
|
||||||
"addressFormat": "Formát adresy",
|
"addressFormat": "Formát adresy",
|
||||||
"notificationSoundsDisabled": "Zakázat zvuky oznámení",
|
"notificationSoundsDisabled": "Zakázat zvuky oznámení",
|
||||||
"applicationName": "Název aplikace",
|
"applicationName": "Název aplikace",
|
||||||
"ldapUsername": "Uživatelské jméno",
|
"ldapUsername": "Uživatelské jméno",
|
||||||
"ldapBindRequiresDn": "Přiřazení vyžaduje Dn",
|
"ldapBindRequiresDn": "Přiřazení vyžaduje Dn",
|
||||||
"ldapBaseDn": "Bázové Dn",
|
"ldapBaseDn": "Bázové Dn",
|
||||||
"ldapUserNameAttribute": "Atribut uživatelského jména",
|
"ldapUserNameAttribute": "Atribut uživatelského jména",
|
||||||
"ldapUserObjectClass": "Třída objektu uživatele",
|
"ldapUserObjectClass": "Třída objektu uživatele",
|
||||||
"ldapUserTitleAttribute": "Atribut názvu uživatele",
|
"ldapUserTitleAttribute": "Atribut názvu uživatele",
|
||||||
"ldapUserFirstNameAttribute": "Atribut křestního jména uživatele",
|
"ldapUserFirstNameAttribute": "Atribut křestního jména uživatele",
|
||||||
"ldapUserLastNameAttribute": "Atribut příjmení uživatele",
|
"ldapUserLastNameAttribute": "Atribut příjmení uživatele",
|
||||||
"ldapUserEmailAddressAttribute": "Atribut emailové adresy uživatele",
|
"ldapUserEmailAddressAttribute": "Atribut emailové adresy uživatele",
|
||||||
"ldapUserTeams": "Týmy uživatele",
|
"ldapUserTeams": "Týmy uživatele",
|
||||||
"ldapUserDefaultTeam": "Výchozí tým uživatele",
|
"ldapUserDefaultTeam": "Výchozí tým uživatele",
|
||||||
"ldapUserPhoneNumberAttribute": "Atribut telefonního čísla uživatele",
|
"ldapUserPhoneNumberAttribute": "Atribut telefonního čísla uživatele",
|
||||||
"assignmentNotificationsEntityList": "Entity k upozornění podle přiřazení",
|
"assignmentNotificationsEntityList": "Entity k upozornění podle přiřazení",
|
||||||
"assignmentEmailNotifications": "Poslat emailová upozornění podle přiřazení",
|
"assignmentEmailNotifications": "Poslat emailová upozornění podle přiřazení",
|
||||||
"assignmentEmailNotificationsEntityList": "Entity k upozornění emailem podle přiřazení",
|
"assignmentEmailNotificationsEntityList": "Entity k upozornění emailem podle přiřazení",
|
||||||
"streamEmailNotifications": "Oznámení o aktualizacích ve streamu pro interní uživatele",
|
"streamEmailNotifications": "Oznámení o aktualizacích ve streamu pro interní uživatele",
|
||||||
"portalStreamEmailNotifications": "Oznámení o aktualizacích ve streamu pro uživatele portálu",
|
"portalStreamEmailNotifications": "Oznámení o aktualizacích ve streamu pro uživatele portálu",
|
||||||
"streamEmailNotificationsEntityList": "Rozsahy emailových oznámení o streamu",
|
"streamEmailNotificationsEntityList": "Rozsahy emailových oznámení o streamu",
|
||||||
"calendarEntityList": "Seznam entit kalendáře",
|
"calendarEntityList": "Seznam entit kalendáře",
|
||||||
"mentionEmailNotifications": "Zasílejte emailová oznámení o nových příspěvcích",
|
"mentionEmailNotifications": "Zasílejte emailová oznámení o nových příspěvcích",
|
||||||
"massEmailDisableMandatoryOptOutLink": "Zakázat povinný odkaz pro odhlášení",
|
"massEmailDisableMandatoryOptOutLink": "Zakázat povinný odkaz pro odhlášení",
|
||||||
"activitiesEntityList": "Seznam entit aktivit",
|
"activitiesEntityList": "Seznam entit aktivit",
|
||||||
"historyEntityList": "Seznam entit historie",
|
"historyEntityList": "Seznam entit historie",
|
||||||
"currencyFormat": "Formát měny",
|
"currencyFormat": "Formát měny",
|
||||||
"currencyDecimalPlaces": "Počet desetinných míst měny",
|
"currencyDecimalPlaces": "Počet desetinných míst měny",
|
||||||
"followCreatedEntities": "Sledovat vytvořené entity",
|
"followCreatedEntities": "Sledovat vytvořené entity",
|
||||||
"aclAllowDeleteCreated": "Povolit odebrání vytvořených záznamů",
|
"aclAllowDeleteCreated": "Povolit odebrání vytvořených záznamů",
|
||||||
"adminNotifications": "Systémová oznámení v administračním panelu",
|
"adminNotifications": "Systémová oznámení v administračním panelu",
|
||||||
"adminNotificationsNewVersion": "Zobrazit oznámení, až bude k dispozici nová verze CRM",
|
"adminNotificationsNewVersion": "Zobrazit oznámení, až bude k dispozici nová verze CRM",
|
||||||
"massEmailMaxPerHourCount": "Maximální počet e-mailů odeslaných za hodinu",
|
"massEmailMaxPerHourCount": "Maximální počet e-mailů odeslaných za hodinu",
|
||||||
"maxEmailAccountCount": "Maximální počet osobních emailových účtů na uživatele",
|
"maxEmailAccountCount": "Maximální počet osobních emailových účtů na uživatele",
|
||||||
"streamEmailNotificationsTypeList": "Na co upozorňovat",
|
"streamEmailNotificationsTypeList": "Na co upozorňovat",
|
||||||
"authTokenPreventConcurrent": "Pouze jeden ověřovací token na uživatele",
|
"authTokenPreventConcurrent": "Pouze jeden ověřovací token na uživatele",
|
||||||
"scopeColorsDisabled": "Zakázat barvy rozsahu",
|
"scopeColorsDisabled": "Zakázat barvy rozsahu",
|
||||||
"tabColorsDisabled": "Zakázat barvy záložek",
|
"tabColorsDisabled": "Zakázat barvy záložek",
|
||||||
"tabIconsDisabled": "Zakázat ikony na kartě",
|
"tabIconsDisabled": "Zakázat ikony na kartě",
|
||||||
"textFilterUseContainsForVarchar": "Při filtrování polí varchar používat operátor „obsahuje“",
|
"textFilterUseContainsForVarchar": "Při filtrování polí varchar používat operátor „obsahuje“",
|
||||||
"emailAddressIsOptedOutByDefault": "Označit nové emailové adresy jako odhlášené",
|
"emailAddressIsOptedOutByDefault": "Označit nové emailové adresy jako odhlášené",
|
||||||
"outboundEmailBccAddress": "Adresa BCC pro externí klienty",
|
"outboundEmailBccAddress": "Adresa BCC pro externí klienty",
|
||||||
"adminNotificationsNewExtensionVersion": "Zobrazit oznámení, když jsou k dispozici nové verze rozšíření",
|
"adminNotificationsNewExtensionVersion": "Zobrazit oznámení, když jsou k dispozici nové verze rozšíření",
|
||||||
"cleanupDeletedRecords": "Vyčistit smazané záznamy",
|
"cleanupDeletedRecords": "Vyčistit smazané záznamy",
|
||||||
"ldapPortalUserLdapAuth": "Pro uživatele portálu použijte ověřování LDAP",
|
"ldapPortalUserLdapAuth": "Pro uživatele portálu použijte ověřování LDAP",
|
||||||
"ldapPortalUserPortals": "Výchozí portály pro uživatele portálu",
|
"ldapPortalUserPortals": "Výchozí portály pro uživatele portálu",
|
||||||
"ldapPortalUserRoles": "Výchozí role pro uživatele portálu",
|
"ldapPortalUserRoles": "Výchozí role pro uživatele portálu",
|
||||||
"fiscalYearShift": "Začátek fiskálního roku",
|
"fiscalYearShift": "Začátek fiskálního roku",
|
||||||
"jobRunInParallel": "Úlohy běží paralelně",
|
"jobRunInParallel": "Úlohy běží paralelně",
|
||||||
"jobMaxPortion": "Maximální velikost části úloh",
|
"jobMaxPortion": "Maximální velikost části úloh",
|
||||||
"jobPoolConcurrencyNumber": "Číslo souběhu úloh",
|
"jobPoolConcurrencyNumber": "Číslo souběhu úloh",
|
||||||
"daemonInterval": "Interval démona",
|
"daemonInterval": "Interval démona",
|
||||||
"daemonMaxProcessNumber": "Maximální počet procesů démona",
|
"daemonMaxProcessNumber": "Maximální počet procesů démona",
|
||||||
"daemonProcessTimeout": "Timeout procesu démona",
|
"daemonProcessTimeout": "Timeout procesu démona",
|
||||||
"addressCityList": "Seznam měst při našeptávání políčka adresa",
|
"addressCityList": "Seznam měst při našeptávání políčka adresa",
|
||||||
"addressStateList": "Seznam států pro našeptávání adres",
|
"addressStateList": "Seznam států pro našeptávání adres",
|
||||||
"cronDisabled": "Zakázat Cron",
|
"cronDisabled": "Zakázat Cron",
|
||||||
"maintenanceMode": "Režim údržby",
|
"maintenanceMode": "Režim údržby",
|
||||||
"useWebSocket": "Použít WebSocket",
|
"useWebSocket": "Použít WebSocket",
|
||||||
"emailNotificationsDelay": "Zpoždění e-mailových oznámení (v sekundách)",
|
"emailNotificationsDelay": "Zpoždění e-mailových oznámení (v sekundách)",
|
||||||
"massEmailOpenTracking": "Sledování otevření emailů",
|
"massEmailOpenTracking": "Sledování otevření emailů",
|
||||||
"passwordRecoveryDisabled": "Zakázat obnovení hesla",
|
"passwordRecoveryDisabled": "Zakázat obnovení hesla",
|
||||||
"passwordRecoveryForAdminDisabled": "Zakázat obnovení hesla pro uživatele správce",
|
"passwordRecoveryForAdminDisabled": "Zakázat obnovení hesla pro uživatele správce",
|
||||||
"passwordGenerateLength": "Délka vygenerovaných hesel",
|
"passwordGenerateLength": "Délka vygenerovaných hesel",
|
||||||
"passwordStrengthLength": "Minimální délka hesla",
|
"passwordStrengthLength": "Minimální délka hesla",
|
||||||
"passwordStrengthLetterCount": "Počet písmen požadovaných v hesle",
|
"passwordStrengthLetterCount": "Počet písmen požadovaných v hesle",
|
||||||
"passwordStrengthNumberCount": "Počet číslic požadovaných v hesle",
|
"passwordStrengthNumberCount": "Počet číslic požadovaných v hesle",
|
||||||
"passwordStrengthBothCases": "Zabraňte vystavení e-mailové adresy ve formuláři pro obnovení hesla",
|
"passwordStrengthBothCases": "Zabraňte vystavení e-mailové adresy ve formuláři pro obnovení hesla",
|
||||||
"auth2FA": "Povolit dvoufaktorové ověřování",
|
"auth2FA": "Povolit dvoufaktorové ověřování",
|
||||||
"auth2FAMethodList": "Dostupné metody dvoufaktorové autorizace",
|
"auth2FAMethodList": "Dostupné metody dvoufaktorové autorizace",
|
||||||
"personNameFormat": "Formát jména osoby",
|
"personNameFormat": "Formát jména osoby",
|
||||||
"newNotificationCountInTitle": "Zobrazit nové číslo oznámení v názvu stránky",
|
"newNotificationCountInTitle": "Zobrazit nové číslo oznámení v názvu stránky",
|
||||||
"massEmailVerp": "Použít VERP",
|
"massEmailVerp": "Použít VERP",
|
||||||
"emailAddressLookupEntityTypeList": "Rozsahy vyhledávání emailových adres",
|
"emailAddressLookupEntityTypeList": "Rozsahy vyhledávání emailových adres",
|
||||||
"busyRangesEntityList": "Seznam volných / zaneprázdněných entit",
|
"busyRangesEntityList": "Seznam volných / zaneprázdněných entit",
|
||||||
"passwordRecoveryForInternalUsersDisabled": "Zakázat obnovení hesla pro uživatele",
|
"passwordRecoveryForInternalUsersDisabled": "Zakázat obnovení hesla pro uživatele",
|
||||||
"passwordRecoveryNoExposure": "Zabraňte vystavení emailové adresy ve formuláři pro obnovení hesla",
|
"passwordRecoveryNoExposure": "Zabraňte vystavení emailové adresy ve formuláři pro obnovení hesla",
|
||||||
"auth2FAForced": "Přimět uživatele k nastavení dvoufaktorové autorizace",
|
"auth2FAForced": "Přimět uživatele k nastavení dvoufaktorové autorizace",
|
||||||
"smsProvider": "Poskytovatel SMS",
|
"smsProvider": "Poskytovatel SMS",
|
||||||
"outboundSmsFromNumber": "SMS z čísla",
|
"outboundSmsFromNumber": "SMS z čísla",
|
||||||
"recordsPerPageSelect": "Záznamy na stránku (Výběr)",
|
"recordsPerPageSelect": "Záznamy na stránku (Výběr)",
|
||||||
"attachmentUploadMaxSize": "Maximální velikost přílohy (Mb)",
|
"attachmentUploadMaxSize": "Maximální velikost přílohy (Mb)",
|
||||||
"attachmentUploadChunkSize": "Velikost části nahrávání příloh (Mb)",
|
"attachmentUploadChunkSize": "Velikost části nahrávání příloh (Mb)",
|
||||||
"workingTimeCalendar": "Pracovní kalendář",
|
"workingTimeCalendar": "Pracovní kalendář",
|
||||||
"oidcClientId": "OIDC ID klienta",
|
"oidcClientId": "OIDC ID klienta",
|
||||||
"oidcClientSecret": "OIDC tajný klíč klienta",
|
"oidcClientSecret": "OIDC tajný klíč klienta",
|
||||||
"oidcAuthorizationRedirectUri": "OIDC URI přesměrování autorizace",
|
"oidcAuthorizationRedirectUri": "OIDC URI přesměrování autorizace",
|
||||||
"oidcAuthorizationEndpoint": "OIDC koncový bod autorizace",
|
"oidcAuthorizationEndpoint": "OIDC koncový bod autorizace",
|
||||||
"oidcTokenEndpoint": "OIDC koncový bod tokenu",
|
"oidcTokenEndpoint": "OIDC koncový bod tokenu",
|
||||||
"oidcJwksEndpoint": "OIDC koncový bod JSON Web Key Set",
|
"oidcJwksEndpoint": "OIDC koncový bod JSON Web Key Set",
|
||||||
"oidcJwtSignatureAlgorithmList": "OIDC povolené podpisové algoritmy JWT",
|
"oidcJwtSignatureAlgorithmList": "OIDC povolené podpisové algoritmy JWT",
|
||||||
"oidcScopes": "OIDC rozsahy",
|
"oidcScopes": "OIDC rozsahy",
|
||||||
"oidcGroupClaim": "OIDC nárok skupiny",
|
"oidcGroupClaim": "OIDC nárok skupiny",
|
||||||
"oidcCreateUser": "OIDC vytvořit uživatele",
|
"oidcCreateUser": "OIDC vytvořit uživatele",
|
||||||
"oidcUsernameClaim": "OIDC nárok uživatelského jména",
|
"oidcUsernameClaim": "OIDC nárok uživatelského jména",
|
||||||
"oidcTeams": "OIDC týmy",
|
"oidcTeams": "OIDC týmy",
|
||||||
"oidcSync": "OIDC synchronizace",
|
"oidcSync": "OIDC synchronizace",
|
||||||
"oidcSyncTeams": "OIDC synchronizace týmů",
|
"oidcSyncTeams": "OIDC synchronizace týmů",
|
||||||
"oidcFallback": "OIDC záložní přihlášení",
|
"oidcFallback": "OIDC záložní přihlášení",
|
||||||
"oidcAllowRegularUserFallback": "OIDC povolit záložní přihlášení běžným uživatelům",
|
"oidcAllowRegularUserFallback": "OIDC povolit záložní přihlášení běžným uživatelům",
|
||||||
"oidcAllowAdminUser": "OIDC povolit přihlášení správcům",
|
"oidcAllowAdminUser": "OIDC povolit přihlášení správcům",
|
||||||
"oidcLogoutUrl": "OIDC URL odhlášení",
|
"oidcLogoutUrl": "OIDC URL odhlášení",
|
||||||
"pdfEngine": "PDF generátor",
|
"pdfEngine": "PDF generátor",
|
||||||
"recordsPerPageKanban": "Záznamy na stránku (Kanban)",
|
"recordsPerPageKanban": "Záznamy na stránku (Kanban)",
|
||||||
"auth2FAInPortal": "Povolit dvoufaktorové ověřování v portálech",
|
"auth2FAInPortal": "Povolit dvoufaktorové ověřování v portálech",
|
||||||
"massEmailMaxPerBatchCount": "Maximální počet e-mailů odeslaných za dávku",
|
"massEmailMaxPerBatchCount": "Maximální počet e-mailů odeslaných za dávku",
|
||||||
"phoneNumberNumericSearch": "Číselné vyhledávání telefonních čísel",
|
"phoneNumberNumericSearch": "Číselné vyhledávání telefonních čísel",
|
||||||
"phoneNumberInternational": "Mezinárodní telefonní čísla",
|
"phoneNumberInternational": "Mezinárodní telefonní čísla",
|
||||||
"phoneNumberPreferredCountryList": "Upřednostňované země pro telefonního čísla",
|
"phoneNumberPreferredCountryList": "Upřednostňované země pro telefonního čísla",
|
||||||
"jobForceUtc": "Vynutit UTC pro úlohy",
|
"jobForceUtc": "Vynutit UTC pro úlohy",
|
||||||
"emailAddressSelectEntityTypeList": "Rozsahy výběru emailových adres",
|
"emailAddressSelectEntityTypeList": "Rozsahy výběru emailových adres",
|
||||||
"phoneNumberExtensions": "Přípony telefonních čísel",
|
"phoneNumberExtensions": "Přípony telefonních čísel",
|
||||||
"oidcAuthorizationPrompt": "OIDC výzva k autorizaci",
|
"oidcAuthorizationPrompt": "OIDC výzva k autorizaci",
|
||||||
"quickSearchFullTextAppendWildcard": "Rychlé vyhledávání přidat wildcard symbol",
|
"quickSearchFullTextAppendWildcard": "Rychlé vyhledávání přidat wildcard symbol",
|
||||||
"authIpAddressCheck": "Omezovat přístup na základě IP adresy",
|
"authIpAddressCheck": "Omezovat přístup na základě IP adresy",
|
||||||
"authIpAddressWhitelist": "Whitelist IP adres",
|
"authIpAddressWhitelist": "Whitelist IP adres",
|
||||||
"authIpAddressCheckExcludedUsers": "Uživatelé vyřazení z kontroly",
|
"authIpAddressCheckExcludedUsers": "Uživatelé vyřazení z kontroly",
|
||||||
"streamEmailWithContentEntityTypeList": "Entity s obsahem emailu v poznámkách streamu",
|
"streamEmailWithContentEntityTypeList": "Entity s obsahem emailu v poznámkách streamu",
|
||||||
"emailScheduledBatchCount": "Maximální počet naplánovaných e-mailů odeslaných za dávku",
|
"emailScheduledBatchCount": "Maximální počet naplánovaných e-mailů odeslaných za dávku",
|
||||||
"passwordStrengthSpecialCharacterCount": "Počet speciálních znaků požadovaných v hesle",
|
"passwordStrengthSpecialCharacterCount": "Počet speciálních znaků požadovaných v hesle",
|
||||||
"availableReactions": "Dostupné reakce",
|
"availableReactions": "Dostupné reakce",
|
||||||
"outboundEmailFromAddress": "Odesílatelská emailová adresa",
|
"outboundEmailFromAddress": "Odesílatelská emailová adresa",
|
||||||
"oidcUserInfoEndpoint": "OIDC koncový bod informací o uživateli",
|
"oidcUserInfoEndpoint": "OIDC koncový bod informací o uživateli",
|
||||||
"baselineRole": "Základní role"
|
"baselineRole": "Základní role"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"recordsPerPage": "Počet záznamů původně zobrazených v zobrazení seznamu.",
|
"recordsPerPage": "Počet záznamů původně zobrazených v zobrazení seznamu.",
|
||||||
"recordsPerPageSmall": "Počet záznamů v panelu vztahů.",
|
"recordsPerPageSmall": "Počet záznamů v panelu vztahů.",
|
||||||
"followCreatedEntities": "Pokud uživatel vytvoří záznam, bude jej sledovat automaticky.",
|
"followCreatedEntities": "Pokud uživatel vytvoří záznam, bude jej sledovat automaticky.",
|
||||||
"ldapUsername": "Úplné uživatelské jméno systému, které umožňuje vyhledávat další uživatele. Např. \"CN = uživatel systému LDAP, OU = uživatelé, OU = espocrm, DC = test, DC = lan\".",
|
"ldapUsername": "Úplné uživatelské jméno systému, které umožňuje vyhledávat další uživatele. Např. \"CN = uživatel systému LDAP, OU = uživatelé, OU = espocrm, DC = test, DC = lan\".",
|
||||||
"ldapPassword": "Heslo pro přístup k serveru LDAP.",
|
"ldapPassword": "Heslo pro přístup k serveru LDAP.",
|
||||||
"ldapAuth": "Přístup k pověření serveru LDAP.",
|
"ldapAuth": "Přístup k pověření serveru LDAP.",
|
||||||
"ldapUserNameAttribute": "Atribut k identifikaci uživatele. \nNapř. „userPrincipalName“ nebo „sAMAccountName“ pro Active Directory, „uid“ pro OpenLDAP.",
|
"ldapUserNameAttribute": "Atribut k identifikaci uživatele. \nNapř. „userPrincipalName“ nebo „sAMAccountName“ pro Active Directory, „uid“ pro OpenLDAP.",
|
||||||
"ldapUserObjectClass": "Atribut ObjectClass pro vyhledávání uživatelů. Např. „osoba“ pro AD, „inetOrgPerson“ pro OpenLDAP.",
|
"ldapUserObjectClass": "Atribut ObjectClass pro vyhledávání uživatelů. Např. „osoba“ pro AD, „inetOrgPerson“ pro OpenLDAP.",
|
||||||
"ldapBindRequiresDn": "Možnost formátovat uživatelské jméno ve formuláři DN.",
|
"ldapBindRequiresDn": "Možnost formátovat uživatelské jméno ve formuláři DN.",
|
||||||
"ldapBaseDn": "Výchozí základní DN používané pro vyhledávání uživatelů. Např. \"OU = uživatelé, OU = espocrm, DC = test, DC = lan\".",
|
"ldapBaseDn": "Výchozí základní DN používané pro vyhledávání uživatelů. Např. \"OU = uživatelé, OU = espocrm, DC = test, DC = lan\".",
|
||||||
"ldapTryUsernameSplit": "Možnost rozdělit uživatelské jméno na doménu.",
|
"ldapTryUsernameSplit": "Možnost rozdělit uživatelské jméno na doménu.",
|
||||||
"ldapOptReferrals": "pokud by měla být sledována doporučení klientovi LDAP.",
|
"ldapOptReferrals": "pokud by měla být sledována doporučení klientovi LDAP.",
|
||||||
"ldapCreateEspoUser": "Tato možnost umožňuje AutoCRM vytvořit uživatele z LDAP.",
|
"ldapCreateEspoUser": "Tato možnost umožňuje EspoCRM vytvořit uživatele z LDAP.",
|
||||||
"ldapUserFirstNameAttribute": "Atribut LDAP, který se používá k určení křestního jména uživatele. Např. \"křestní jméno\".",
|
"ldapUserFirstNameAttribute": "Atribut LDAP, který se používá k určení křestního jména uživatele. Např. \"křestní jméno\".",
|
||||||
"ldapUserLastNameAttribute": "Atribut LDAP, který se používá k určení příjmení uživatele. Např. \"sn\".",
|
"ldapUserLastNameAttribute": "Atribut LDAP, který se používá k určení příjmení uživatele. Např. \"sn\".",
|
||||||
"ldapUserTitleAttribute": "LDAP atribut pro titul uživatele.",
|
"ldapUserTitleAttribute": "LDAP atribut pro titul uživatele.",
|
||||||
"ldapUserEmailAddressAttribute": "Atribut LDAP, který se používá k určení e-mailové adresy uživatele. Např. \"pošta\".",
|
"ldapUserEmailAddressAttribute": "Atribut LDAP, který se používá k určení e-mailové adresy uživatele. Např. \"pošta\".",
|
||||||
"ldapUserPhoneNumberAttribute": "LDAP atribut pro telefonní číslo uživatele.",
|
"ldapUserPhoneNumberAttribute": "LDAP atribut pro telefonní číslo uživatele.",
|
||||||
"ldapUserLoginFilter": "Filtr, který umožňuje omezit uživatele, kteří mohou používat AutoCRM. Např. \"memberOf = CN = espoGroup, OU = groups, OU = espocrm, DC = test, DC = lan\".",
|
"ldapUserLoginFilter": "Filtr, který umožňuje omezit uživatele, kteří mohou používat EspoCRM. Např. \"memberOf = CN = espoGroup, OU = groups, OU = espocrm, DC = test, DC = lan\".",
|
||||||
"ldapAccountDomainName": "Doména, která se používá k autorizaci k serveru LDAP.",
|
"ldapAccountDomainName": "Doména, která se používá k autorizaci k serveru LDAP.",
|
||||||
"ldapAccountDomainNameShort": "Krátká doména, která se používá k autorizaci k serveru LDAP.",
|
"ldapAccountDomainNameShort": "Krátká doména, která se používá k autorizaci k serveru LDAP.",
|
||||||
"ldapUserTeams": "LDAP týmy pro uživatele.",
|
"ldapUserTeams": "LDAP týmy pro uživatele.",
|
||||||
"ldapUserDefaultTeam": "Výchozí tým pro vytvořeného uživatele. Další informace najdete v uživatelském profilu.",
|
"ldapUserDefaultTeam": "Výchozí tým pro vytvořeného uživatele. Další informace najdete v uživatelském profilu.",
|
||||||
"b2cMode": "Ve výchozím nastavení je AutoCRM přizpůsoben pro B2B. Můžete jej přepnout na B2C.",
|
"b2cMode": "Ve výchozím nastavení je EspoCRM přizpůsoben pro B2B. Můžete jej přepnout na B2C.",
|
||||||
"aclStrictMode": "Povoleno: Přístup k rozsahům bude zakázán, pokud není uveden v rolích. \nZakázán: Přístup k rozsahům bude povolen, pokud není uveden v rolích.",
|
"aclStrictMode": "Povoleno: Přístup k rozsahům bude zakázán, pokud není uveden v rolích. \nZakázán: Přístup k rozsahům bude povolen, pokud není uveden v rolích.",
|
||||||
"outboundEmailIsShared": "Povolit posílání emailů uživatelům pomocí SMTP.",
|
"outboundEmailIsShared": "Povolit posílání emailů uživatelům pomocí SMTP.",
|
||||||
"streamEmailNotificationsEntityList": "Emailová upozornění na aktualizace streamu sledovaných záznamů. Uživatelé budou dostávat e-mailová oznámení pouze pro určené typy entit.",
|
"streamEmailNotificationsEntityList": "Emailová upozornění na aktualizace streamu sledovaných záznamů. Uživatelé budou dostávat e-mailová oznámení pouze pro určené typy entit.",
|
||||||
"authTokenPreventConcurrent": "Uživatelé nebudou moci být přihlášeni na více zařízeních současně.",
|
"authTokenPreventConcurrent": "Uživatelé nebudou moci být přihlášeni na více zařízeních současně.",
|
||||||
"ldapPortalUserLdapAuth": "Umožněte uživatelům portálu používat autentizaci LDAP namísto autentizace Auto.",
|
"ldapPortalUserLdapAuth": "Umožněte uživatelům portálu používat autentizaci LDAP namísto autentizace Auto.",
|
||||||
"ldapPortalUserPortals": "Výchozí portály pro vytvořeného uživatele portálu",
|
"ldapPortalUserPortals": "Výchozí portály pro vytvořeného uživatele portálu",
|
||||||
"ldapPortalUserRoles": "Výchozí role pro vytvořeného uživatele portálu",
|
"ldapPortalUserRoles": "Výchozí role pro vytvořeného uživatele portálu",
|
||||||
"jobPoolConcurrencyNumber": "Maximální počet procesů spuštěných současně.",
|
"jobPoolConcurrencyNumber": "Maximální počet procesů spuštěných současně.",
|
||||||
"cronDisabled": "Cron se nespustí.",
|
"cronDisabled": "Cron se nespustí.",
|
||||||
"maintenanceMode": "Do systému budou mít přístup pouze správci.",
|
"maintenanceMode": "Do systému budou mít přístup pouze správci.",
|
||||||
"ldapAccountCanonicalForm": "Typ kanonického formuláře vašeho účtu. K dispozici jsou 4 možnosti: \n- „Dn“ - formulář ve formátu „CN = tester, OU = espocrm, DC = test, DC = lan“. - „Uživatelské jméno“ - formulář „tester“ .- „Zpětné lomítko“ - formulář „SPOLEČNOST \\ tester“. - „Principal“ - formulář „tester@company.com“.",
|
"ldapAccountCanonicalForm": "Typ kanonického formuláře vašeho účtu. K dispozici jsou 4 možnosti: \n- „Dn“ - formulář ve formátu „CN = tester, OU = espocrm, DC = test, DC = lan“. - „Uživatelské jméno“ - formulář „tester“ .- „Zpětné lomítko“ - formulář „SPOLEČNOST \\ tester“. - „Principal“ - formulář „tester@company.com“.",
|
||||||
"massEmailVerp": "Variabilní zpětná cesta obálky. Pro lepší zpracování odražených zpráv. Ujistěte se, že to váš poskytovatel SMTP podporuje.",
|
"massEmailVerp": "Variabilní zpětná cesta obálky. Pro lepší zpracování odražených zpráv. Ujistěte se, že to váš poskytovatel SMTP podporuje.",
|
||||||
"addressStateList": "Návrhy států pro adresní pole.",
|
"addressStateList": "Návrhy států pro adresní pole.",
|
||||||
"addressCityList": "Návrhy měst pro adresní pole.",
|
"addressCityList": "Návrhy měst pro adresní pole.",
|
||||||
"addressCountryList": "Návrhy zemí pro adresní pole.",
|
"addressCountryList": "Návrhy zemí pro adresní pole.",
|
||||||
"exportDisabled": "Zakázat export pro běžné uživatele.",
|
"exportDisabled": "Zakázat export pro běžné uživatele.",
|
||||||
"siteUrl": "URL vašeho CRM systému.",
|
"siteUrl": "URL vašeho CRM systému.",
|
||||||
"useCache": "Nedoporučuje se deaktivovat, pokud se nejedná o účely vývoje.",
|
"useCache": "Nedoporučuje se deaktivovat, pokud se nejedná o účely vývoje.",
|
||||||
"useWebSocket": "WebSocket umožňuje obousměrnou interaktivní komunikaci mezi serverem a prohlížečem. Vyžaduje nastavení démonu WebSocket na vašem serveru. Pro více informací se podívejte do dokumentace.",
|
"useWebSocket": "WebSocket umožňuje obousměrnou interaktivní komunikaci mezi serverem a prohlížečem. Vyžaduje nastavení démonu WebSocket na vašem serveru. Pro více informací se podívejte do dokumentace.",
|
||||||
"emailNotificationsDelay": "Zprávu lze upravit ve stanoveném časovém rámci před odesláním oznámení.",
|
"emailNotificationsDelay": "Zprávu lze upravit ve stanoveném časovém rámci před odesláním oznámení.",
|
||||||
"recordsPerPageSelect": "Počet záznamů na stránku ve výběru.",
|
"recordsPerPageSelect": "Počet záznamů na stránku ve výběru.",
|
||||||
"workingTimeCalendar": "Pracovní kalendář pro zobrazení pracovní doby.",
|
"workingTimeCalendar": "Pracovní kalendář pro zobrazení pracovní doby.",
|
||||||
"oidcFallback": "Povolit záložní přihlášení.",
|
"oidcFallback": "Povolit záložní přihlášení.",
|
||||||
"oidcCreateUser": "Automaticky vytvářet nové uživatele z OIDC.",
|
"oidcCreateUser": "Automaticky vytvářet nové uživatele z OIDC.",
|
||||||
"oidcSync": "Synchronizovat uživatelské údaje z OIDC.",
|
"oidcSync": "Synchronizovat uživatelské údaje z OIDC.",
|
||||||
"oidcSyncTeams": "Synchronizovat týmy z OIDC.",
|
"oidcSyncTeams": "Synchronizovat týmy z OIDC.",
|
||||||
"oidcUsernameClaim": "OIDC nárok pro uživatelské jméno.",
|
"oidcUsernameClaim": "OIDC nárok pro uživatelské jméno.",
|
||||||
"oidcTeams": "OIDC týmy pro uživatele.",
|
"oidcTeams": "OIDC týmy pro uživatele.",
|
||||||
"recordsPerPageKanban": "Počet záznamů na stránku v Kanban zobrazení.",
|
"recordsPerPageKanban": "Počet záznamů na stránku v Kanban zobrazení.",
|
||||||
"jobForceUtc": "Použije časové pásmo UTC pro plánované úlohy. Jinak bude použito časové pásmo nastavené v nastavení.",
|
"jobForceUtc": "Použije časové pásmo UTC pro plánované úlohy. Jinak bude použito časové pásmo nastavené v nastavení.",
|
||||||
"authIpAddressCheckExcludedUsers": "Uživatelé, kteří se budou moci přihlásit z jakéhokoli místa.",
|
"authIpAddressCheckExcludedUsers": "Uživatelé, kteří se budou moci přihlásit z jakéhokoli místa.",
|
||||||
"authIpAddressWhitelist": "Seznam IP adres nebo rozsahů v notaci CIDR.\n\nPortály nejsou omezeny.",
|
"authIpAddressWhitelist": "Seznam IP adres nebo rozsahů v notaci CIDR.\n\nPortály nejsou omezeny.",
|
||||||
"oidcGroupClaim": "OIDC nárok pro skupinové informace.",
|
"oidcGroupClaim": "OIDC nárok pro skupinové informace.",
|
||||||
"outboundEmailFromAddress": "Systémová emailová adresa.",
|
"outboundEmailFromAddress": "Systémová emailová adresa.",
|
||||||
"baselineRole": "Základní role definuje minimální úroveň přístupových práv pro všechny uživatele. Tato role je automaticky aplikována na všechny uživatele bez ohledu na jejich ostatní role.",
|
"baselineRole": "Základní role definuje minimální úroveň přístupových práv pro všechny uživatele. Tato role je automaticky aplikována na všechny uživatele bez ohledu na jejich ostatní role.",
|
||||||
"displayListViewRecordCount": "Zobrazit celkový počet záznamů v zobrazení seznamu.",
|
"displayListViewRecordCount": "Zobrazit celkový počet záznamů v zobrazení seznamu.",
|
||||||
"currencyList": "Dostupné měny v systému.",
|
"currencyList": "Dostupné měny v systému.",
|
||||||
"activitiesEntityList": "Entity, které se považují za aktivity.",
|
"activitiesEntityList": "Entity, které se považují za aktivity.",
|
||||||
"historyEntityList": "Entity, které se považují za historii.",
|
"historyEntityList": "Entity, které se považují za historii.",
|
||||||
"calendarEntityList": "Entity zobrazené v kalendáři.",
|
"calendarEntityList": "Entity zobrazené v kalendáři.",
|
||||||
"globalSearchEntityList": "Entity dostupné v globálním vyhledávání.",
|
"globalSearchEntityList": "Entity dostupné v globálním vyhledávání.",
|
||||||
"passwordRecoveryForInternalUsersDisabled": "Obnovit heslo budou moci pouze uživatelé portálu.",
|
"passwordRecoveryForInternalUsersDisabled": "Obnovit heslo budou moci pouze uživatelé portálu.",
|
||||||
"passwordRecoveryNoExposure": "Nebude možné určit, zda je v systému zaregistrována konkrétní e-mailová adresa.",
|
"passwordRecoveryNoExposure": "Nebude možné určit, zda je v systému zaregistrována konkrétní e-mailová adresa.",
|
||||||
"emailAddressLookupEntityTypeList": "Pro automatické vyplňování emailových adres.",
|
"emailAddressLookupEntityTypeList": "Pro automatické vyplňování emailových adres.",
|
||||||
"emailAddressSelectEntityTypeList": "Rozsahy pro výběr emailových adres.",
|
"emailAddressSelectEntityTypeList": "Rozsahy pro výběr emailových adres.",
|
||||||
"busyRangesEntityList": "Co se bude brát v úvahu při zobrazování časových období zaneprázdnění v plánovači a časové ose.",
|
"busyRangesEntityList": "Co se bude brát v úvahu při zobrazování časových období zaneprázdnění v plánovači a časové ose.",
|
||||||
"emailMessageMaxSize": "Všechny příchozí emaily přesahující stanovenou velikost budou načteny bez těla a příloh.",
|
"emailMessageMaxSize": "Všechny příchozí emaily přesahující stanovenou velikost budou načteny bez těla a příloh.",
|
||||||
"authTokenLifetime": "Definuje, jak dlouho mohou existovat tokeny. \n0 - znamená žádné vypršení platnosti.",
|
"authTokenLifetime": "Definuje, jak dlouho mohou existovat tokeny. \n0 - znamená žádné vypršení platnosti.",
|
||||||
"authTokenMaxIdleTime": "Definuje, jak dlouho mohou existovat poslední přístupové tokeny. \n0 - znamená žádné vypršení platnosti.",
|
"authTokenMaxIdleTime": "Definuje, jak dlouho mohou existovat poslední přístupové tokeny. \n0 - znamená žádné vypršení platnosti.",
|
||||||
"userThemesDisabled": "Pokud je zaškrtnuto, uživatelé nebudou moci vybrat jiné téma.",
|
"userThemesDisabled": "Pokud je zaškrtnuto, uživatelé nebudou moci vybrat jiné téma.",
|
||||||
"currencyDecimalPlaces": "Počet desetinných míst. Pokud jsou prázdné, zobrazí se všechna neprázdná desetinná místa.",
|
"currencyDecimalPlaces": "Počet desetinných míst. Pokud jsou prázdné, zobrazí se všechna neprázdná desetinná místa.",
|
||||||
"aclAllowDeleteCreated": "Uživatelé budou moci odebrat záznamy, které vytvořili, i když nemají přístup k odstranění.",
|
"aclAllowDeleteCreated": "Uživatelé budou moci odebrat záznamy, které vytvořili, i když nemají přístup k odstranění.",
|
||||||
"textFilterUseContainsForVarchar": "Pokud není zaškrtnuto, použije se operátor „začíná na“. Můžete použít zástupný znak '%'.",
|
"textFilterUseContainsForVarchar": "Pokud není zaškrtnuto, použije se operátor „začíná na“. Můžete použít zástupný znak '%'.",
|
||||||
"emailAddressIsOptedOutByDefault": "Při vytváření nového záznamu bude emailová adresa označena jako odhlášena.",
|
"emailAddressIsOptedOutByDefault": "Při vytváření nového záznamu bude emailová adresa označena jako odhlášena.",
|
||||||
"cleanupDeletedRecords": "Odebrané záznamy budou po chvíli z databáze odstraněny.",
|
"cleanupDeletedRecords": "Odebrané záznamy budou po chvíli z databáze odstraněny.",
|
||||||
"jobRunInParallel": "Úlohy budou prováděny paralelně.",
|
"jobRunInParallel": "Úlohy budou prováděny paralelně.",
|
||||||
"jobMaxPortion": "Maximální počet zpracovaných úloh na jedno provedení.",
|
"jobMaxPortion": "Maximální počet zpracovaných úloh na jedno provedení.",
|
||||||
"daemonInterval": "Interval spouštění démona v sekundách.",
|
"daemonInterval": "Interval spouštění démona v sekundách.",
|
||||||
"daemonMaxProcessNumber": "Maximální počet procesů cron běžících současně.",
|
"daemonMaxProcessNumber": "Maximální počet procesů cron běžících současně.",
|
||||||
"daemonProcessTimeout": "Maximální doba provedení (v sekundách) přidělená jednomu procesu cron.",
|
"daemonProcessTimeout": "Maximální doba provedení (v sekundách) přidělená jednomu procesu cron.",
|
||||||
"oidcLogoutUrl": "URL pro odhlášení z OIDC poskytovatele.",
|
"oidcLogoutUrl": "URL pro odhlášení z OIDC poskytovatele.",
|
||||||
"quickSearchFullTextAppendWildcard": "Připojte zástupný znak k dotazu automatického dokončování, pokud je povoleno fulltextové vyhledávání. Snižuje to výkon vyhledávání."
|
"quickSearchFullTextAppendWildcard": "Připojte zástupný znak k dotazu automatického dokončování, pokud je povoleno fulltextové vyhledávání. Snižuje to výkon vyhledávání."
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"System": "Systém",
|
"System": "Systém",
|
||||||
"Locale": "Lokalizace",
|
"Locale": "Lokalizace",
|
||||||
"Configuration": "Konfigurace",
|
"Configuration": "Konfigurace",
|
||||||
"In-app Notifications": "In-app notifikace",
|
"In-app Notifications": "In-app notifikace",
|
||||||
"Email Notifications": "Email notifikace",
|
"Email Notifications": "Email notifikace",
|
||||||
"Currency Settings": "Nastavení měn",
|
"Currency Settings": "Nastavení měn",
|
||||||
"Currency Rates": "Kurzy měn",
|
"Currency Rates": "Kurzy měn",
|
||||||
"Mass Email": "Hromadný email",
|
"Mass Email": "Hromadný email",
|
||||||
"Test Connection": "Test připojení",
|
"Test Connection": "Test připojení",
|
||||||
"Connecting": "Připojování...",
|
"Connecting": "Připojování...",
|
||||||
"Activities": "Aktivity",
|
"Activities": "Aktivity",
|
||||||
"Admin Notifications": "Oznámení správce",
|
"Admin Notifications": "Oznámení správce",
|
||||||
"Search": "Vyhledat",
|
"Search": "Vyhledat",
|
||||||
"Misc": "Vedlejší",
|
"Misc": "Vedlejší",
|
||||||
"Passwords": "Hesla",
|
"Passwords": "Hesla",
|
||||||
"2-Factor Authentication": "Dvoufaktorové ověřování",
|
"2-Factor Authentication": "Dvoufaktorové ověřování",
|
||||||
"Group Tab": "Skupina záložek",
|
"Group Tab": "Skupina záložek",
|
||||||
"Attachments": "Přílohy",
|
"Attachments": "Přílohy",
|
||||||
"IdP Group": "IdP skupina",
|
"IdP Group": "IdP skupina",
|
||||||
"Divider": "Oddělovač",
|
"Divider": "Oddělovač",
|
||||||
"General": "Obecné",
|
"General": "Obecné",
|
||||||
"Navbar": "Navigační panel",
|
"Navbar": "Navigační panel",
|
||||||
"Phone Numbers": "Telefonní čísla",
|
"Phone Numbers": "Telefonní čísla",
|
||||||
"Access": "Přístup",
|
"Access": "Přístup",
|
||||||
"Strength": "Síla",
|
"Strength": "Síla",
|
||||||
"Recovery": "Obnovení",
|
"Recovery": "Obnovení",
|
||||||
"Scheduled Send": "Naplánované odeslání"
|
"Scheduled Send": "Naplánované odeslání"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"ldapTestConnection": "Připojení bylo úspěšně navázáno.",
|
"ldapTestConnection": "Připojení bylo úspěšně navázáno.",
|
||||||
"confirmBaselineRoleChange": "Opravdu chcete změnit základní roli? Tato změna ovlivní přístupová práva všech uživatelů."
|
"confirmBaselineRoleChange": "Opravdu chcete změnit základní roli? Tato změna ovlivní přístupová práva všech uživatelů."
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"streamEmailNotificationsTypeList": {
|
"streamEmailNotificationsTypeList": {
|
||||||
"Post": "Příspěvky",
|
"Post": "Příspěvky",
|
||||||
"Status": "Aktualizace stavu",
|
"Status": "Aktualizace stavu",
|
||||||
"EmailReceived": "Přijaté emaily"
|
"EmailReceived": "Přijaté emaily"
|
||||||
},
|
},
|
||||||
"personNameFormat": {
|
"personNameFormat": {
|
||||||
"firstLast": "Jméno Příjmení",
|
"firstLast": "Jméno Příjmení",
|
||||||
"lastFirst": "Příjmení Jméno",
|
"lastFirst": "Příjmení Jméno",
|
||||||
"firstMiddleLast": "Jméno Prostřední jméno Příjmení",
|
"firstMiddleLast": "Jméno Prostřední jméno Příjmení",
|
||||||
"lastFirstMiddle": "Příjmení Jméno Prostřední jméno"
|
"lastFirstMiddle": "Příjmení Jméno Prostřední jméno"
|
||||||
},
|
},
|
||||||
"auth2FAMethodList": {
|
"auth2FAMethodList": {
|
||||||
"Email": "E-mail"
|
"Email": "E-mail"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,16 @@
|
|||||||
"default": "Active"
|
"default": "Active"
|
||||||
},
|
},
|
||||||
"host": {
|
"host": {
|
||||||
"type": "varchar"
|
"type": "varchar",
|
||||||
|
"massUpdateDisabled": true
|
||||||
},
|
},
|
||||||
"port": {
|
"port": {
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 65535,
|
"max": 65535,
|
||||||
"default": 993,
|
"default": 993,
|
||||||
"disableFormatting": true
|
"disableFormatting": true,
|
||||||
|
"massUpdateDisabled": true
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"type": "enum",
|
"type": "enum",
|
||||||
@@ -112,14 +114,16 @@
|
|||||||
"tooltip": true
|
"tooltip": true
|
||||||
},
|
},
|
||||||
"smtpHost": {
|
"smtpHost": {
|
||||||
"type": "varchar"
|
"type": "varchar",
|
||||||
|
"massUpdateDisabled": true
|
||||||
},
|
},
|
||||||
"smtpPort": {
|
"smtpPort": {
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 65535,
|
"max": 65535,
|
||||||
"default": 587,
|
"default": 587,
|
||||||
"disableFormatting": true
|
"disableFormatting": true,
|
||||||
|
"massUpdateDisabled": true
|
||||||
},
|
},
|
||||||
"smtpAuth": {
|
"smtpAuth": {
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
|
|||||||
@@ -21,14 +21,16 @@
|
|||||||
"default": "Active"
|
"default": "Active"
|
||||||
},
|
},
|
||||||
"host": {
|
"host": {
|
||||||
"type": "varchar"
|
"type": "varchar",
|
||||||
|
"massUpdateDisabled": true
|
||||||
},
|
},
|
||||||
"port": {
|
"port": {
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 65535,
|
"max": 65535,
|
||||||
"default": 993,
|
"default": 993,
|
||||||
"disableFormatting": true
|
"disableFormatting": true,
|
||||||
|
"massUpdateDisabled": true
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"type": "enum",
|
"type": "enum",
|
||||||
@@ -122,14 +124,16 @@
|
|||||||
"tooltip": true
|
"tooltip": true
|
||||||
},
|
},
|
||||||
"smtpHost": {
|
"smtpHost": {
|
||||||
"type": "varchar"
|
"type": "varchar",
|
||||||
|
"massUpdateDisabled": true
|
||||||
},
|
},
|
||||||
"smtpPort": {
|
"smtpPort": {
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 65535,
|
"max": 65535,
|
||||||
"default": 587,
|
"default": 587,
|
||||||
"disableFormatting": true
|
"disableFormatting": true,
|
||||||
|
"massUpdateDisabled": true
|
||||||
},
|
},
|
||||||
"smtpAuth": {
|
"smtpAuth": {
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
"type": "varchar",
|
"type": "varchar",
|
||||||
"maxLength": 512,
|
"maxLength": 512,
|
||||||
"required": true,
|
"required": true,
|
||||||
"copyToClipboard": true
|
"copyToClipboard": true,
|
||||||
|
"validatorClassNameList": [
|
||||||
|
"Espo\\Classes\\FieldValidators\\Webhook\\Url\\NotInternal"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"isActive": {
|
"isActive": {
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
|
|||||||
@@ -15,9 +15,11 @@
|
|||||||
],
|
],
|
||||||
"beforeCreateHookClassNameList": [
|
"beforeCreateHookClassNameList": [
|
||||||
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeCreate",
|
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeCreate",
|
||||||
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeSave"
|
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeSave",
|
||||||
|
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeSaveValidateHosts"
|
||||||
],
|
],
|
||||||
"beforeUpdateHookClassNameList": [
|
"beforeUpdateHookClassNameList": [
|
||||||
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeSave"
|
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeSave",
|
||||||
|
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeSaveValidateHosts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,5 +18,11 @@
|
|||||||
],
|
],
|
||||||
"listLoaderClassNameList": [
|
"listLoaderClassNameList": [
|
||||||
"Espo\\Classes\\FieldProcessing\\InboundEmail\\IsSystemLoader"
|
"Espo\\Classes\\FieldProcessing\\InboundEmail\\IsSystemLoader"
|
||||||
|
],
|
||||||
|
"beforeCreateHookClassNameList": [
|
||||||
|
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeSaveValidateHosts"
|
||||||
|
],
|
||||||
|
"beforeUpdateHookClassNameList": [
|
||||||
|
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeSaveValidateHosts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,15 @@ use stdClass;
|
|||||||
|
|
||||||
class SettingsService
|
class SettingsService
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var string[]
|
||||||
|
* @todo Do not use when these parameters moved away from the settings.
|
||||||
|
*/
|
||||||
|
private array $ignoreUpdateParamList = [
|
||||||
|
'loginView',
|
||||||
|
'loginData',
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ApplicationState $applicationState,
|
private ApplicationState $applicationState,
|
||||||
private Config $config,
|
private Config $config,
|
||||||
@@ -87,11 +96,22 @@ class SettingsService
|
|||||||
$this->filterData($data);
|
$this->filterData($data);
|
||||||
$this->loadAdditionalParams($data);
|
$this->loadAdditionalParams($data);
|
||||||
|
|
||||||
|
/** @noinspection PhpDeprecationInspection */
|
||||||
|
$metadataData = $this->getMetadataConfigData();
|
||||||
|
|
||||||
|
foreach (get_object_vars($metadataData) as $key => $value) {
|
||||||
|
$data->$key = $value;
|
||||||
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get metadata to be used in config.
|
* Get metadata to be used in config.
|
||||||
|
*
|
||||||
|
* @todo Make private in v9.4.0.
|
||||||
|
* @todo Move away from settings. Use some different approach.
|
||||||
|
* @deprecated Since v9.3.2.
|
||||||
*/
|
*/
|
||||||
public function getMetadataConfigData(): stdClass
|
public function getMetadataConfigData(): stdClass
|
||||||
{
|
{
|
||||||
@@ -208,6 +228,7 @@ class SettingsService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$ignoreItemList = array_merge(
|
$ignoreItemList = array_merge(
|
||||||
|
$this->ignoreUpdateParamList,
|
||||||
$this->access->getSystemParamList(),
|
$this->access->getSystemParamList(),
|
||||||
$this->access->getReadOnlyParamList(),
|
$this->access->getReadOnlyParamList(),
|
||||||
$this->isRestrictedMode() && !$user->isSuperAdmin() ?
|
$this->isRestrictedMode() && !$user->isSuperAdmin() ?
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class UploadUrlService
|
|||||||
*/
|
*/
|
||||||
public function uploadImage(string $url, FieldData $data): Attachment
|
public function uploadImage(string $url, FieldData $data): Attachment
|
||||||
{
|
{
|
||||||
if (!$this->urlCheck->isNotInternalUrl($url)) {
|
if (!$this->urlCheck->isUrlAndNotIternal($url)) {
|
||||||
throw new ForbiddenSilent("Not allowed URL.");
|
throw new ForbiddenSilent("Not allowed URL.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,9 +114,20 @@ class UploadUrlService
|
|||||||
/**
|
/**
|
||||||
* @param non-empty-string $url
|
* @param non-empty-string $url
|
||||||
* @return ?array{string, string} A type and contents.
|
* @return ?array{string, string} A type and contents.
|
||||||
|
* @throws ForbiddenSilent
|
||||||
*/
|
*/
|
||||||
private function getImageDataByUrl(string $url): ?array
|
private function getImageDataByUrl(string $url): ?array
|
||||||
{
|
{
|
||||||
|
$resolve = $this->urlCheck->getCurlResolve($url);
|
||||||
|
|
||||||
|
if ($resolve === []) {
|
||||||
|
throw new ForbiddenSilent("Could not resolve the host.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($resolve !== null && !$this->urlCheck->validateCurlResolveNotInternal($resolve)) {
|
||||||
|
throw new ForbiddenSilent("Forbidden host.");
|
||||||
|
}
|
||||||
|
|
||||||
$type = null;
|
$type = null;
|
||||||
|
|
||||||
if (!function_exists('curl_init')) {
|
if (!function_exists('curl_init')) {
|
||||||
@@ -144,6 +155,10 @@ class UploadUrlService
|
|||||||
$opts[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTPS | \CURLPROTO_HTTP;
|
$opts[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTPS | \CURLPROTO_HTTP;
|
||||||
$opts[\CURLOPT_REDIR_PROTOCOLS] = \CURLPROTO_HTTPS;
|
$opts[\CURLOPT_REDIR_PROTOCOLS] = \CURLPROTO_HTTPS;
|
||||||
|
|
||||||
|
if ($resolve) {
|
||||||
|
$opts[CURLOPT_RESOLVE] = $resolve;
|
||||||
|
}
|
||||||
|
|
||||||
$ch = curl_init();
|
$ch = curl_init();
|
||||||
|
|
||||||
curl_setopt_array($ch, $opts);
|
curl_setopt_array($ch, $opts);
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ use Espo\Core\Api\Response;
|
|||||||
use Espo\Core\Api\ResponseComposer;
|
use Espo\Core\Api\ResponseComposer;
|
||||||
use Espo\Core\Exceptions\BadRequest;
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
use Espo\Core\Exceptions\Forbidden;
|
use Espo\Core\Exceptions\Forbidden;
|
||||||
|
use Espo\Core\Exceptions\NotFound;
|
||||||
|
use Espo\Entities\Attachment;
|
||||||
use Espo\Entities\Email;
|
use Espo\Entities\Email;
|
||||||
use Espo\Entities\User;
|
use Espo\Entities\User;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
use Espo\Tools\Email\ImportEmlService;
|
use Espo\Tools\Email\ImportEmlService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,6 +52,7 @@ class PostImportEml implements Action
|
|||||||
private Acl $acl,
|
private Acl $acl,
|
||||||
private User $user,
|
private User $user,
|
||||||
private ImportEmlService $service,
|
private ImportEmlService $service,
|
||||||
|
private EntityManager $entityManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function process(Request $request): Response
|
public function process(Request $request): Response
|
||||||
@@ -61,11 +65,32 @@ class PostImportEml implements Action
|
|||||||
throw new BadRequest("No 'fileId'.");
|
throw new BadRequest("No 'fileId'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$email = $this->service->import($fileId, $this->user->getId());
|
$attachment = $this->getAttachment($fileId);
|
||||||
|
|
||||||
|
$email = $this->service->import($attachment, $this->user->getId());
|
||||||
|
|
||||||
return ResponseComposer::json(['id' => $email->getId()]);
|
return ResponseComposer::json(['id' => $email->getId()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotFound
|
||||||
|
* @throws Forbidden
|
||||||
|
*/
|
||||||
|
private function getAttachment(string $fileId): Attachment
|
||||||
|
{
|
||||||
|
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getById($fileId);
|
||||||
|
|
||||||
|
if (!$attachment) {
|
||||||
|
throw new NotFound("Attachment not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->acl->checkEntityRead($attachment)) {
|
||||||
|
throw new Forbidden("No access to attachment.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attachment;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Forbidden
|
* @throws Forbidden
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ use Espo\Core\Exceptions\BadRequest;
|
|||||||
use Espo\Core\Exceptions\Error;
|
use Espo\Core\Exceptions\Error;
|
||||||
use Espo\Core\Exceptions\Forbidden;
|
use Espo\Core\Exceptions\Forbidden;
|
||||||
use Espo\Core\Exceptions\NotFound;
|
use Espo\Core\Exceptions\NotFound;
|
||||||
|
use Espo\Core\Mail\Account\Util\AddressUtil;
|
||||||
use Espo\Core\Mail\Exceptions\NoSmtp;
|
use Espo\Core\Mail\Exceptions\NoSmtp;
|
||||||
use Espo\Core\Mail\SmtpParams;
|
use Espo\Core\Mail\SmtpParams;
|
||||||
|
use Espo\Core\Utils\Security\HostCheck;
|
||||||
use Espo\Entities\Email;
|
use Espo\Entities\Email;
|
||||||
use Espo\Tools\Email\SendService;
|
use Espo\Tools\Email\SendService;
|
||||||
use Espo\Tools\Email\TestSendData;
|
use Espo\Tools\Email\TestSendData;
|
||||||
@@ -51,7 +53,9 @@ class PostSendTest implements Action
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private SendService $sendService,
|
private SendService $sendService,
|
||||||
private Acl $acl
|
private Acl $acl,
|
||||||
|
private HostCheck $hostCheck,
|
||||||
|
private AddressUtil $addressUtil,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,6 +113,13 @@ class PostSendTest implements Action
|
|||||||
->withAuthMechanism($authMechanism);
|
->withAuthMechanism($authMechanism);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!$this->addressUtil->isAllowedAddress($smtpParams) &&
|
||||||
|
!$this->hostCheck->isHostAndNotInternal($server)
|
||||||
|
) {
|
||||||
|
throw new Forbidden("Not allowed internal host.");
|
||||||
|
}
|
||||||
|
|
||||||
$data = new TestSendData($emailAddress, $type, $id, $userId);
|
$data = new TestSendData($emailAddress, $type, $id, $userId);
|
||||||
|
|
||||||
$this->sendService->sendTestEmail($smtpParams, $data);
|
$this->sendService->sendTestEmail($smtpParams, $data);
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ namespace Espo\Tools\Email;
|
|||||||
|
|
||||||
use Espo\Core\Exceptions\Conflict;
|
use Espo\Core\Exceptions\Conflict;
|
||||||
use Espo\Core\Exceptions\Error;
|
use Espo\Core\Exceptions\Error;
|
||||||
use Espo\Core\Exceptions\NotFound;
|
|
||||||
use Espo\Core\FileStorage\Manager;
|
use Espo\Core\FileStorage\Manager;
|
||||||
use Espo\Core\Mail\Exceptions\ImapError;
|
use Espo\Core\Mail\Exceptions\ImapError;
|
||||||
use Espo\Core\Mail\Importer;
|
use Espo\Core\Mail\Importer;
|
||||||
@@ -56,16 +55,13 @@ class ImportEmlService
|
|||||||
/**
|
/**
|
||||||
* Import an EML.
|
* Import an EML.
|
||||||
*
|
*
|
||||||
* @param string $fileId An attachment ID.
|
|
||||||
* @param ?string $userId A user ID to relate an email with.
|
* @param ?string $userId A user ID to relate an email with.
|
||||||
* @return Email An Email.
|
* @return Email An Email.
|
||||||
* @throws NotFound
|
|
||||||
* @throws Error
|
* @throws Error
|
||||||
* @throws Conflict
|
* @throws Conflict
|
||||||
*/
|
*/
|
||||||
public function import(string $fileId, ?string $userId = null): Email
|
public function import(Attachment $attachment, ?string $userId = null): Email
|
||||||
{
|
{
|
||||||
$attachment = $this->getAttachment($fileId);
|
|
||||||
$contents = $this->fileStorageManager->getContents($attachment);
|
$contents = $this->fileStorageManager->getContents($attachment);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -93,20 +89,6 @@ class ImportEmlService
|
|||||||
return $email;
|
return $email;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws NotFound
|
|
||||||
*/
|
|
||||||
private function getAttachment(string $fileId): Attachment
|
|
||||||
{
|
|
||||||
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getById($fileId);
|
|
||||||
|
|
||||||
if (!$attachment) {
|
|
||||||
throw new NotFound("Attachment not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $attachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Conflict
|
* @throws Conflict
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ use Espo\Core\Notification\EmailNotificationHandler;
|
|||||||
use Espo\Core\Mail\SenderParams;
|
use Espo\Core\Mail\SenderParams;
|
||||||
use Espo\Core\Utils\Config\ApplicationConfig;
|
use Espo\Core\Utils\Config\ApplicationConfig;
|
||||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||||
|
use Espo\Core\Utils\Markdown\Markdown;
|
||||||
use Espo\Entities\Note;
|
use Espo\Entities\Note;
|
||||||
use Espo\ORM\Collection;
|
use Espo\ORM\Collection;
|
||||||
use Espo\Repositories\Portal as PortalRepository;
|
use Espo\Repositories\Portal as PortalRepository;
|
||||||
@@ -58,8 +59,6 @@ use Espo\Core\Utils\TemplateFileManager;
|
|||||||
use Espo\Core\Utils\Util;
|
use Espo\Core\Utils\Util;
|
||||||
use Espo\Tools\Stream\NoteAccessControl;
|
use Espo\Tools\Stream\NoteAccessControl;
|
||||||
|
|
||||||
use Michelf\Markdown;
|
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
@@ -325,11 +324,10 @@ class Processor
|
|||||||
|
|
||||||
$data['userName'] = $note->get('createdByName');
|
$data['userName'] = $note->get('createdByName');
|
||||||
|
|
||||||
$post = Markdown::defaultTransform(
|
$post = $note->getPost() ?? '';
|
||||||
$note->get('post') ?? ''
|
|
||||||
);
|
|
||||||
|
|
||||||
$data['post'] = $post;
|
|
||||||
|
$data['post'] = Markdown::transform($post);
|
||||||
|
|
||||||
$subjectTpl = $this->templateFileManager->getTemplate('mention', 'subject');
|
$subjectTpl = $this->templateFileManager->getTemplate('mention', 'subject');
|
||||||
$bodyTpl = $this->templateFileManager->getTemplate('mention', 'body');
|
$bodyTpl = $this->templateFileManager->getTemplate('mention', 'body');
|
||||||
@@ -486,9 +484,7 @@ class Processor
|
|||||||
|
|
||||||
$data['userName'] = $note->get('createdByName');
|
$data['userName'] = $note->get('createdByName');
|
||||||
|
|
||||||
$post = Markdown::defaultTransform($note->getPost() ?? '');
|
$data['post'] = Markdown::transform($note->getPost() ?? '');
|
||||||
|
|
||||||
$data['post'] = $post;
|
|
||||||
|
|
||||||
$parent = null;
|
$parent = null;
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ class Service
|
|||||||
$builder->order('relevance', Order::DESC);
|
$builder->order('relevance', Order::DESC);
|
||||||
}
|
}
|
||||||
|
|
||||||
$builder->order('order', Order::DESC);
|
$builder->order('order');
|
||||||
$builder->order(Field::NAME);
|
$builder->order(Field::NAME);
|
||||||
|
|
||||||
$unionQuery = $builder->build();
|
$unionQuery = $builder->build();
|
||||||
|
|||||||
@@ -855,7 +855,9 @@ class LinkManager
|
|||||||
"links.$link",
|
"links.$link",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->metadata->delete('clientDefs', $entity, ["dynamicLogic.fields.$link"]);
|
$this->metadata->delete('logicDefs', $entity, [
|
||||||
|
"fields.$link",
|
||||||
|
]);
|
||||||
|
|
||||||
$this->metadata->save();
|
$this->metadata->save();
|
||||||
|
|
||||||
@@ -907,8 +909,13 @@ class LinkManager
|
|||||||
->build();
|
->build();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->metadata->delete('clientDefs', $entity, ["dynamicLogic.fields.$link"]);
|
$this->metadata->delete('logicDefs', $entity, [
|
||||||
$this->metadata->delete('clientDefs', $entityForeign, ["dynamicLogic.fields.$linkForeign"]);
|
"fields.$link",
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->metadata->delete('logicDefs', $entityForeign, [
|
||||||
|
"fields.$linkForeign",
|
||||||
|
]);
|
||||||
|
|
||||||
$this->metadata->delete('entityDefs', $entity, [
|
$this->metadata->delete('entityDefs', $entity, [
|
||||||
"fields.$link",
|
"fields.$link",
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class RecordService
|
|||||||
];
|
];
|
||||||
|
|
||||||
if ($this->user->isPortal()) {
|
if ($this->user->isPortal()) {
|
||||||
$where[] = ['isInternal' => true];
|
$where[] = ['isInternal' => false];
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->applyPortalAccess($builder, $where);
|
$this->applyPortalAccess($builder, $where);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
|||||||
/*! espocrm 2026-02-19 */
|
/*! espocrm 2026-03-24 */
|
||||||
define("modules/crm/views/scheduler/scheduler",["exports","view","vis-data","vis-timeline","moment","jquery"],function(t,e,a,s,n,r){Object.defineProperty(t,"__esModule",{value:!0});t.default=void 0;e=i(e);n=i(n);r=i(r);function i(t){return t&&t.__esModule?t:{default:t}}class o extends e.default{templateContent=`
|
define("modules/crm/views/scheduler/scheduler",["exports","view","vis-data","vis-timeline","moment","jquery"],function(t,e,a,s,n,r){Object.defineProperty(t,"__esModule",{value:!0});t.default=void 0;e=i(e);n=i(n);r=i(r);function i(t){return t&&t.__esModule?t:{default:t}}class o extends e.default{templateContent=`
|
||||||
<div class="timeline"></div>
|
<div class="timeline"></div>
|
||||||
<link href="{{basePath}}client/modules/crm/css/vis.css" rel="stylesheet">
|
<link href="{{basePath}}client/modules/crm/css/vis.css" rel="stylesheet">
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
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`
|
||||||
71
custom/Espo/Custom/Api/JunctionData/GetDokumentes.php
Normal file
71
custom/Espo/Custom/Api/JunctionData/GetDokumentes.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Espo\Custom\Api\JunctionData;
|
||||||
|
|
||||||
|
use Espo\Core\Api\Action;
|
||||||
|
use Espo\Core\Api\Request;
|
||||||
|
use Espo\Core\Api\Response;
|
||||||
|
use Espo\Core\Api\ResponseComposer;
|
||||||
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
|
use Espo\Core\Exceptions\NotFound;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/JunctionData/CAIKnowledge/:knowledgeId/dokumentes
|
||||||
|
*
|
||||||
|
* Returns all documents linked to a knowledge entry with junction table data
|
||||||
|
*/
|
||||||
|
class GetDokumentes implements Action
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Request $request): Response
|
||||||
|
{
|
||||||
|
$knowledgeId = $request->getRouteParam('knowledgeId');
|
||||||
|
|
||||||
|
if (!$knowledgeId) {
|
||||||
|
throw new BadRequest('Knowledge ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify knowledge exists
|
||||||
|
$knowledge = $this->entityManager->getEntityById('CAIKnowledge', $knowledgeId);
|
||||||
|
if (!$knowledge) {
|
||||||
|
throw new NotFound('Knowledge entry not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
j.id as junctionId,
|
||||||
|
j.c_a_i_knowledge_id as cAIKnowledgeId,
|
||||||
|
j.c_dokumente_id as cDokumenteId,
|
||||||
|
j.ai_document_id as aiDocumentId,
|
||||||
|
j.syncstatus,
|
||||||
|
j.last_sync as lastSync,
|
||||||
|
d.id as documentId,
|
||||||
|
d.name as documentName,
|
||||||
|
d.blake3hash as blake3hash,
|
||||||
|
d.created_at as documentCreatedAt,
|
||||||
|
d.modified_at as documentModifiedAt
|
||||||
|
FROM c_a_i_knowledge_dokumente j
|
||||||
|
INNER JOIN c_dokumente d ON j.c_dokumente_id = d.id
|
||||||
|
WHERE j.c_a_i_knowledge_id = :knowledgeId
|
||||||
|
AND j.deleted = 0
|
||||||
|
AND d.deleted = 0
|
||||||
|
ORDER BY j.id DESC
|
||||||
|
";
|
||||||
|
|
||||||
|
$sth = $pdo->prepare($sql);
|
||||||
|
$sth->execute(['knowledgeId' => $knowledgeId]);
|
||||||
|
|
||||||
|
$results = $sth->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
return ResponseComposer::json([
|
||||||
|
'total' => count($results),
|
||||||
|
'list' => $results
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
178
custom/Espo/Custom/Api/JunctionData/LinkDokument.php
Normal file
178
custom/Espo/Custom/Api/JunctionData/LinkDokument.php
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Espo\Custom\Api\JunctionData;
|
||||||
|
|
||||||
|
use Espo\Core\Api\Action;
|
||||||
|
use Espo\Core\Api\Request;
|
||||||
|
use Espo\Core\Api\Response;
|
||||||
|
use Espo\Core\Api\ResponseComposer;
|
||||||
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
|
use Espo\Core\Exceptions\NotFound;
|
||||||
|
use Espo\Core\Exceptions\Conflict;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/v1/JunctionData/CAIKnowledge/:knowledgeId/dokumentes/:documentId
|
||||||
|
*
|
||||||
|
* Creates or updates relationship with junction table data
|
||||||
|
* This endpoint links the entities AND sets junction columns in one call
|
||||||
|
*/
|
||||||
|
class LinkDokument implements Action
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Request $request): Response
|
||||||
|
{
|
||||||
|
$knowledgeId = $request->getRouteParam('knowledgeId');
|
||||||
|
$documentId = $request->getRouteParam('documentId');
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
|
||||||
|
if (!$knowledgeId || !$documentId) {
|
||||||
|
throw new BadRequest('Knowledge ID and Document ID are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify entities exist
|
||||||
|
$knowledge = $this->entityManager->getEntityById('CAIKnowledge', $knowledgeId);
|
||||||
|
if (!$knowledge) {
|
||||||
|
throw new NotFound('Knowledge entry not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$document = $this->entityManager->getEntityById('CDokumente', $documentId);
|
||||||
|
if (!$document) {
|
||||||
|
throw new NotFound('Document not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
// Check if link already exists
|
||||||
|
$existing = $this->checkIfLinked($knowledgeId, $documentId);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
// Link exists - update junction columns
|
||||||
|
return $this->updateExisting($knowledgeId, $documentId, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new link via ORM (triggers hooks like DokumenteSyncStatus)
|
||||||
|
$this->entityManager->getRDBRepository('CAIKnowledge')
|
||||||
|
->getRelation($knowledge, 'dokumentes')
|
||||||
|
->relate($document);
|
||||||
|
|
||||||
|
// Now set junction columns if provided
|
||||||
|
if (!empty((array)$data)) {
|
||||||
|
return $this->updateExisting($knowledgeId, $documentId, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return created entry
|
||||||
|
$result = $this->getJunctionEntry($knowledgeId, $documentId);
|
||||||
|
|
||||||
|
return ResponseComposer::json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkIfLinked(string $knowledgeId, string $documentId): bool
|
||||||
|
{
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM c_a_i_knowledge_dokumente
|
||||||
|
WHERE c_a_i_knowledge_id = :knowledgeId
|
||||||
|
AND c_dokumente_id = :documentId
|
||||||
|
AND deleted = 0
|
||||||
|
";
|
||||||
|
|
||||||
|
$sth = $pdo->prepare($sql);
|
||||||
|
$sth->execute([
|
||||||
|
'knowledgeId' => $knowledgeId,
|
||||||
|
'documentId' => $documentId
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $sth->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
return $result['count'] > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateExisting(string $knowledgeId, string $documentId, \stdClass $data): Response
|
||||||
|
{
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
// Build dynamic UPDATE SET clause
|
||||||
|
$setClauses = [];
|
||||||
|
$params = [
|
||||||
|
'knowledgeId' => $knowledgeId,
|
||||||
|
'documentId' => $documentId
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($data->aiDocumentId)) {
|
||||||
|
$setClauses[] = "ai_document_id = :aiDocumentId";
|
||||||
|
$params['aiDocumentId'] = $data->aiDocumentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data->syncstatus)) {
|
||||||
|
$allowedStatuses = ['new', 'unclean', 'synced', 'failed', 'unsupported'];
|
||||||
|
if (!in_array($data->syncstatus, $allowedStatuses)) {
|
||||||
|
throw new BadRequest('Invalid syncstatus value. Allowed: ' . implode(', ', $allowedStatuses));
|
||||||
|
}
|
||||||
|
$setClauses[] = "syncstatus = :syncstatus";
|
||||||
|
$params['syncstatus'] = $data->syncstatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data->lastSync)) {
|
||||||
|
$setClauses[] = "last_sync = :lastSync";
|
||||||
|
$params['lastSync'] = $data->lastSync;
|
||||||
|
} elseif (isset($data->updateLastSync) && $data->updateLastSync === true) {
|
||||||
|
$setClauses[] = "last_sync = NOW()";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($setClauses)) {
|
||||||
|
$sql = "
|
||||||
|
UPDATE c_a_i_knowledge_dokumente
|
||||||
|
SET " . implode(', ', $setClauses) . "
|
||||||
|
WHERE c_a_i_knowledge_id = :knowledgeId
|
||||||
|
AND c_dokumente_id = :documentId
|
||||||
|
AND deleted = 0
|
||||||
|
";
|
||||||
|
|
||||||
|
$sth = $pdo->prepare($sql);
|
||||||
|
$sth->execute($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return updated data
|
||||||
|
$result = $this->getJunctionEntry($knowledgeId, $documentId);
|
||||||
|
|
||||||
|
return ResponseComposer::json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getJunctionEntry(string $knowledgeId, string $documentId): array
|
||||||
|
{
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
id as junctionId,
|
||||||
|
c_a_i_knowledge_id as cAIKnowledgeId,
|
||||||
|
c_dokumente_id as cDokumenteId,
|
||||||
|
ai_document_id as aiDocumentId,
|
||||||
|
syncstatus,
|
||||||
|
last_sync as lastSync
|
||||||
|
FROM c_a_i_knowledge_dokumente
|
||||||
|
WHERE c_a_i_knowledge_id = :knowledgeId
|
||||||
|
AND c_dokumente_id = :documentId
|
||||||
|
AND deleted = 0
|
||||||
|
";
|
||||||
|
|
||||||
|
$sth = $pdo->prepare($sql);
|
||||||
|
$sth->execute([
|
||||||
|
'knowledgeId' => $knowledgeId,
|
||||||
|
'documentId' => $documentId
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $sth->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
throw new NotFound('Junction entry not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
123
custom/Espo/Custom/Api/JunctionData/UpdateJunction.php
Normal file
123
custom/Espo/Custom/Api/JunctionData/UpdateJunction.php
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Espo\Custom\Api\JunctionData;
|
||||||
|
|
||||||
|
use Espo\Core\Api\Action;
|
||||||
|
use Espo\Core\Api\Request;
|
||||||
|
use Espo\Core\Api\Response;
|
||||||
|
use Espo\Core\Api\ResponseComposer;
|
||||||
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
|
use Espo\Core\Exceptions\NotFound;
|
||||||
|
use Espo\ORM\EntityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/v1/JunctionData/CAIKnowledge/:knowledgeId/dokumentes/:documentId
|
||||||
|
*
|
||||||
|
* Updates junction table columns for an existing relationship
|
||||||
|
*/
|
||||||
|
class UpdateJunction implements Action
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function process(Request $request): Response
|
||||||
|
{
|
||||||
|
$knowledgeId = $request->getRouteParam('knowledgeId');
|
||||||
|
$documentId = $request->getRouteParam('documentId');
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
|
||||||
|
if (!$knowledgeId || !$documentId) {
|
||||||
|
throw new BadRequest('Knowledge ID and Document ID are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
// Build dynamic UPDATE SET clause
|
||||||
|
$setClauses = [];
|
||||||
|
$params = [
|
||||||
|
'knowledgeId' => $knowledgeId,
|
||||||
|
'documentId' => $documentId
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($data->aiDocumentId)) {
|
||||||
|
$setClauses[] = "ai_document_id = :aiDocumentId";
|
||||||
|
$params['aiDocumentId'] = $data->aiDocumentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data->syncstatus)) {
|
||||||
|
$allowedStatuses = ['new', 'unclean', 'synced', 'failed', 'unsupported'];
|
||||||
|
if (!in_array($data->syncstatus, $allowedStatuses)) {
|
||||||
|
throw new BadRequest('Invalid syncstatus value. Allowed: ' . implode(', ', $allowedStatuses));
|
||||||
|
}
|
||||||
|
$setClauses[] = "syncstatus = :syncstatus";
|
||||||
|
$params['syncstatus'] = $data->syncstatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data->lastSync)) {
|
||||||
|
$setClauses[] = "last_sync = :lastSync";
|
||||||
|
$params['lastSync'] = $data->lastSync;
|
||||||
|
} elseif (isset($data->updateLastSync) && $data->updateLastSync === true) {
|
||||||
|
$setClauses[] = "last_sync = NOW()";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($setClauses)) {
|
||||||
|
throw new BadRequest('No fields to update. Provide at least one of: aiDocumentId, syncstatus, lastSync');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
UPDATE c_a_i_knowledge_dokumente
|
||||||
|
SET " . implode(', ', $setClauses) . "
|
||||||
|
WHERE c_a_i_knowledge_id = :knowledgeId
|
||||||
|
AND c_dokumente_id = :documentId
|
||||||
|
AND deleted = 0
|
||||||
|
";
|
||||||
|
|
||||||
|
$sth = $pdo->prepare($sql);
|
||||||
|
$sth->execute($params);
|
||||||
|
|
||||||
|
$affectedRows = $sth->rowCount();
|
||||||
|
|
||||||
|
if ($affectedRows === 0) {
|
||||||
|
throw new NotFound('Junction entry not found or no changes made');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return updated data
|
||||||
|
$result = $this->getJunctionEntry($knowledgeId, $documentId);
|
||||||
|
|
||||||
|
return ResponseComposer::json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getJunctionEntry(string $knowledgeId, string $documentId): array
|
||||||
|
{
|
||||||
|
$pdo = $this->entityManager->getPDO();
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
id as junctionId,
|
||||||
|
c_a_i_knowledge_id as cAIKnowledgeId,
|
||||||
|
c_dokumente_id as cDokumenteId,
|
||||||
|
ai_document_id as aiDocumentId,
|
||||||
|
syncstatus,
|
||||||
|
last_sync as lastSync
|
||||||
|
FROM c_a_i_knowledge_dokumente
|
||||||
|
WHERE c_a_i_knowledge_id = :knowledgeId
|
||||||
|
AND c_dokumente_id = :documentId
|
||||||
|
AND deleted = 0
|
||||||
|
";
|
||||||
|
|
||||||
|
$sth = $pdo->prepare($sql);
|
||||||
|
$sth->execute([
|
||||||
|
'knowledgeId' => $knowledgeId,
|
||||||
|
'documentId' => $documentId
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $sth->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
throw new NotFound('Junction entry not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
custom/Espo/Custom/Controllers/CAICollectionCDokumente.php
Normal file
23
custom/Espo/Custom/Controllers/CAICollectionCDokumente.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Controllers;
|
||||||
|
|
||||||
|
use Espo\Core\Controllers\Record;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Junction Controller: CAICollection ↔ CDokumente
|
||||||
|
*
|
||||||
|
* Provides REST API access to the junction table with additionalColumns:
|
||||||
|
* - xaifileid: XAI external file ID
|
||||||
|
* - syncStatus: Sync state tracking
|
||||||
|
*/
|
||||||
|
class CAICollectionCDokumente extends Record
|
||||||
|
{
|
||||||
|
// Inherits all CRUD operations from Record controller
|
||||||
|
//
|
||||||
|
// Available endpoints:
|
||||||
|
// GET /api/v1/CAICollectionCDokumente
|
||||||
|
// GET /api/v1/CAICollectionCDokumente/{id}
|
||||||
|
// POST /api/v1/CAICollectionCDokumente
|
||||||
|
// PUT /api/v1/CAICollectionCDokumente/{id}
|
||||||
|
// DELETE /api/v1/CAICollectionCDokumente/{id}
|
||||||
|
}
|
||||||
7
custom/Espo/Custom/Controllers/CAIKnowledge.php
Normal file
7
custom/Espo/Custom/Controllers/CAIKnowledge.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Espo\Custom\Controllers;
|
||||||
|
|
||||||
|
class CAIKnowledge extends \Espo\Core\Templates\Controllers\Base
|
||||||
|
{
|
||||||
|
}
|
||||||
24
custom/Espo/Custom/Controllers/CAIKnowledgeCDokumente.php
Normal file
24
custom/Espo/Custom/Controllers/CAIKnowledgeCDokumente.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Controllers;
|
||||||
|
|
||||||
|
use Espo\Core\Controllers\Record;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Junction Controller: CAIKnowledge ↔ CDokumente
|
||||||
|
*
|
||||||
|
* Provides REST API access to the junction table with additionalColumns:
|
||||||
|
* - aiDocumentId: External AI document reference
|
||||||
|
* - syncstatus: Sync state tracking (new, unclean, synced, failed)
|
||||||
|
* - lastSync: Last synchronization timestamp
|
||||||
|
*/
|
||||||
|
class CAIKnowledgeCDokumente extends Record
|
||||||
|
{
|
||||||
|
// Inherits all CRUD operations from Record controller
|
||||||
|
//
|
||||||
|
// Available endpoints:
|
||||||
|
// GET /api/v1/CAIKnowledgeCDokumente
|
||||||
|
// GET /api/v1/CAIKnowledgeCDokumente/{id}
|
||||||
|
// POST /api/v1/CAIKnowledgeCDokumente
|
||||||
|
// PUT /api/v1/CAIKnowledgeCDokumente/{id}
|
||||||
|
// DELETE /api/v1/CAIKnowledgeCDokumente/{id}
|
||||||
|
}
|
||||||
7
custom/Espo/Custom/Controllers/CAdvowareAkten.php
Normal file
7
custom/Espo/Custom/Controllers/CAdvowareAkten.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Espo\Custom\Controllers;
|
||||||
|
|
||||||
|
class CAdvowareAkten extends \Espo\Core\Templates\Controllers\BasePlus
|
||||||
|
{
|
||||||
|
}
|
||||||
23
custom/Espo/Custom/Controllers/CAdvowareAktenCDokumente.php
Normal file
23
custom/Espo/Custom/Controllers/CAdvowareAktenCDokumente.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Controllers;
|
||||||
|
|
||||||
|
use Espo\Core\Controllers\Record;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Junction Controller: CAdvowareAkten ↔ CDokumente
|
||||||
|
*
|
||||||
|
* Provides REST API access to the junction table with additionalColumns:
|
||||||
|
* - hnr: Advoware HNR reference
|
||||||
|
* - syncStatus: Sync state tracking
|
||||||
|
*/
|
||||||
|
class CAdvowareAktenCDokumente extends Record
|
||||||
|
{
|
||||||
|
// Inherits all CRUD operations from Record controller
|
||||||
|
//
|
||||||
|
// Available endpoints:
|
||||||
|
// GET /api/v1/CAdvowareAktenCDokumente
|
||||||
|
// GET /api/v1/CAdvowareAktenCDokumente/{id}
|
||||||
|
// POST /api/v1/CAdvowareAktenCDokumente
|
||||||
|
// PUT /api/v1/CAdvowareAktenCDokumente/{id}
|
||||||
|
// DELETE /api/v1/CAdvowareAktenCDokumente/{id}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CAIKnowledge;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\ORM\Repository\Option\SaveOptions;
|
||||||
|
use Espo\Core\Hook\Hook\BeforeSave;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Prüft Junction-Table und aktualisiert globalen syncStatus
|
||||||
|
* basierend auf den syncstatus-Werten der verknüpften Dokumente
|
||||||
|
*/
|
||||||
|
class CheckGlobalSyncStatus implements BeforeSave
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function beforeSave(Entity $entity, SaveOptions $options): void
|
||||||
|
{
|
||||||
|
// Überspringe, wenn skipHooks gesetzt ist (verhindert Loops)
|
||||||
|
if ($options->get('skipHooks')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur wenn Entity bereits existiert (nicht bei Create)
|
||||||
|
if ($entity->isNew()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hole alle verknüpften Dokumente mit ihren syncstatus-Werten aus der Junction-Tabelle
|
||||||
|
$query = $this->entityManager->getQueryBuilder()
|
||||||
|
->select(['syncstatus'])
|
||||||
|
->from('CAIKnowledgeDokumente')
|
||||||
|
->where([
|
||||||
|
'cAIKnowledgeId' => $entity->getId(),
|
||||||
|
'deleted' => false
|
||||||
|
])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$pdoStatement = $this->entityManager->getQueryExecutor()->execute($query);
|
||||||
|
$rows = $pdoStatement->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Wenn keine Dokumente verknüpft, setze auf "unclean"
|
||||||
|
if (empty($rows)) {
|
||||||
|
$entity->set('syncStatus', 'unclean');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe, ob irgendein Dokument "new" oder "unclean" ist
|
||||||
|
$hasUnsynced = false;
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$status = $row['syncstatus'] ?? null;
|
||||||
|
if ($status === 'new' || $status === 'unclean' || $status === null || $status === '') {
|
||||||
|
$hasUnsynced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setze globalen Status
|
||||||
|
if ($hasUnsynced) {
|
||||||
|
$entity->set('syncStatus', 'unclean');
|
||||||
|
} else {
|
||||||
|
// Alle Dokumente sind "synced"
|
||||||
|
$entity->set('syncStatus', 'synced');
|
||||||
|
$entity->set('lastSync', date('Y-m-d H:i:s'));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Bei Fehler loggen und Status auf "unclean" setzen
|
||||||
|
$GLOBALS['log']->error('CAIKnowledge CheckGlobalSyncStatus Hook Error: ' . $e->getMessage());
|
||||||
|
$entity->set('syncStatus', 'unclean');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CAIKnowledge;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterRelate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Setzt Dokument-Sync-Status auf "new" beim Verknüpfen und
|
||||||
|
* globalen syncStatus auf "unclean"
|
||||||
|
*/
|
||||||
|
class DokumenteSyncStatus implements AfterRelate
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterRelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
array $columnData,
|
||||||
|
\Espo\ORM\Repository\Option\RelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentes-Beziehung
|
||||||
|
if ($relationName !== 'dokumentes') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setze Sync-Status des Dokuments in der Junction-Tabelle auf "new"
|
||||||
|
$repository = $this->entityManager->getRDBRepository('CAIKnowledge');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$repository->getRelation($entity, 'dokumentes')->updateColumns(
|
||||||
|
$foreignEntity,
|
||||||
|
['syncstatus' => 'new']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Setze globalen syncStatus auf "unclean"
|
||||||
|
$entity->set('syncStatus', 'unclean');
|
||||||
|
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Fehler loggen, aber nicht werfen (um Verknüpfung nicht zu blockieren)
|
||||||
|
$GLOBALS['log']->error('CAIKnowledge DokumenteSyncStatus Hook Error: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
182
custom/Espo/Custom/Hooks/CAIKnowledge/PropagateDocumentsUp.php
Normal file
182
custom/Espo/Custom/Hooks/CAIKnowledge/PropagateDocumentsUp.php
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CAIKnowledge;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterRelate;
|
||||||
|
use Espo\Core\Hook\Hook\AfterUnrelate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Propagiert Dokumenten-Verknüpfungen von AIKnowledge nach oben zu Räumungsklage/Mietinkasso
|
||||||
|
*
|
||||||
|
* Wenn Dokument mit AIKnowledge verknüpft wird:
|
||||||
|
* → verknüpfe mit verbundener Räumungsklage/Mietinkasso
|
||||||
|
* → von dort propagiert es automatisch zu AdvowareAkten (via deren Hooks)
|
||||||
|
*
|
||||||
|
* Wenn Dokument von AIKnowledge entknüpft wird:
|
||||||
|
* → entknüpfe von verbundener Räumungsklage/Mietinkasso
|
||||||
|
* → von dort propagiert es automatisch von AdvowareAkten (via deren Hooks)
|
||||||
|
*/
|
||||||
|
class PropagateDocumentsUp implements AfterRelate, AfterUnrelate
|
||||||
|
{
|
||||||
|
private static array $processing = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterRelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
array $columnData,
|
||||||
|
\Espo\ORM\Repository\Option\RelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentes-Beziehung
|
||||||
|
if ($relationName !== 'dokumentes') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-relate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prüfe ob Räumungsklage verknüpft ist
|
||||||
|
$raumungsklage = $this->entityManager
|
||||||
|
->getRDBRepository('CAIKnowledge')
|
||||||
|
->getRelation($entity, 'vmhRumungsklage')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($raumungsklage) {
|
||||||
|
$this->relateDocument($raumungsklage, 'dokumentesvmhraumungsklage', $foreignEntity);
|
||||||
|
|
||||||
|
// Also link to AdvowareAkte if Räumungsklage has one
|
||||||
|
$advowareAkte = $this->entityManager
|
||||||
|
->getRDBRepository('CVmhRumungsklage')
|
||||||
|
->getRelation($raumungsklage, 'advowareAkten')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($advowareAkte && !$foreignEntity->get('cAdvowareAktenId')) {
|
||||||
|
$foreignEntity->set('cAdvowareAktenId', $advowareAkte->getId());
|
||||||
|
$foreignEntity->set('syncStatus', 'new');
|
||||||
|
$this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob Mietinkasso verknüpft ist
|
||||||
|
$mietinkasso = $this->entityManager
|
||||||
|
->getRDBRepository('CAIKnowledge')
|
||||||
|
->getRelation($entity, 'mietinkasso')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($mietinkasso) {
|
||||||
|
$this->relateDocument($mietinkasso, 'dokumentesmietinkasso', $foreignEntity);
|
||||||
|
|
||||||
|
// Also link to AdvowareAkte if Mietinkasso has one
|
||||||
|
$advowareAkte = $this->entityManager
|
||||||
|
->getRDBRepository('CMietinkasso')
|
||||||
|
->getRelation($mietinkasso, 'advowareAkten')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($advowareAkte && !$foreignEntity->get('cAdvowareAktenId')) {
|
||||||
|
$foreignEntity->set('cAdvowareAktenId', $advowareAkte->getId());
|
||||||
|
$foreignEntity->set('syncStatus', 'new');
|
||||||
|
$this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CAIKnowledge PropagateDocumentsUp (relate) Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function afterUnrelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
\Espo\ORM\Repository\Option\UnrelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentes-Beziehung
|
||||||
|
if ($relationName !== 'dokumentes') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-unrelate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prüfe ob Räumungsklage verknüpft ist
|
||||||
|
$raumungsklage = $this->entityManager
|
||||||
|
->getRDBRepository('CAIKnowledge')
|
||||||
|
->getRelation($entity, 'vmhRumungsklage')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($raumungsklage) {
|
||||||
|
$this->unrelateDocument($raumungsklage, 'dokumentesvmhraumungsklage', $foreignEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob Mietinkasso verknüpft ist
|
||||||
|
$mietinkasso = $this->entityManager
|
||||||
|
->getRDBRepository('CAIKnowledge')
|
||||||
|
->getRelation($entity, 'mietinkasso')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($mietinkasso) {
|
||||||
|
$this->unrelateDocument($mietinkasso, 'dokumentesmietinkasso', $foreignEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We don't remove cAdvowareAktenId on unrelate from AIKnowledge
|
||||||
|
// because the document might still be linked to Räumungsklage/Mietinkasso
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CAIKnowledge PropagateDocumentsUp (unrelate) Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Verknüpfe Dokument (nur wenn nicht bereits verknüpft)
|
||||||
|
*/
|
||||||
|
private function relateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob bereits verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if (!$isRelated) {
|
||||||
|
$relation->relate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Entknüpfe Dokument
|
||||||
|
*/
|
||||||
|
private function unrelateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($isRelated) {
|
||||||
|
$relation->unrelate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
custom/Espo/Custom/Hooks/CAdvowareAkten/PropagateDocumentsUp.php
Normal file
118
custom/Espo/Custom/Hooks/CAdvowareAkten/PropagateDocumentsUp.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CAdvowareAkten;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterSave;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Propagiert Dokumenten-Änderungen von AdvowareAkten nach oben zu Räumungsklage/Mietinkasso
|
||||||
|
* und auch zu AICollection
|
||||||
|
*
|
||||||
|
* Wenn ein Dokument einer AdvowareAkte zugewiesen wird (via cAdvowareAktenId):
|
||||||
|
* → verknüpfe mit verbundener Räumungsklage/Mietinkasso
|
||||||
|
* → verknüpfe mit AICollection
|
||||||
|
*
|
||||||
|
* Improved logic: Works with direct belongsTo relationship (cAdvowareAktenId)
|
||||||
|
*/
|
||||||
|
class PropagateDocumentsUp implements AfterSave
|
||||||
|
{
|
||||||
|
private static array $processing = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterSave(Entity $entity, \Espo\ORM\Repository\Option\SaveOptions $options): void
|
||||||
|
{
|
||||||
|
// Only process when cAdvowareAktenId changed
|
||||||
|
if (!$entity->isAttributeChanged('cAdvowareAktenId')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$akteId = $entity->get('cAdvowareAktenId');
|
||||||
|
if (!$akteId) {
|
||||||
|
return; // Document was unlinked from Akte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $akteId . '-' . $entity->getId() . '-propagate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load AdvowareAkte
|
||||||
|
$akte = $this->entityManager->getEntity('CAdvowareAkten', $akteId);
|
||||||
|
if (!$akte) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob Räumungsklage verknüpft ist
|
||||||
|
$raumungsklage = $this->entityManager
|
||||||
|
->getRDBRepository('CAdvowareAkten')
|
||||||
|
->getRelation($akte, 'vmhRumungsklage')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($raumungsklage) {
|
||||||
|
$this->relateDocument($raumungsklage, 'dokumentesvmhraumungsklage', $entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob Mietinkasso verknüpft ist
|
||||||
|
$mietinkasso = $this->entityManager
|
||||||
|
->getRDBRepository('CAdvowareAkten')
|
||||||
|
->getRelation($akte, 'mietinkasso')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($mietinkasso) {
|
||||||
|
$this->relateDocument($mietinkasso, 'dokumentesmietinkasso', $entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also propagate to AICollection if Räumungsklage or Mietinkasso has one
|
||||||
|
if ($raumungsklage) {
|
||||||
|
$aiKnowledge = $this->entityManager
|
||||||
|
->getRDBRepository('CVmhRumungsklage')
|
||||||
|
->getRelation($raumungsklage, 'aIKnowledge')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($aiKnowledge) {
|
||||||
|
$this->relateDocument($aiKnowledge, 'dokumentes', $entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mietinkasso) {
|
||||||
|
$aiKnowledge = $this->entityManager
|
||||||
|
->getRDBRepository('CMietinkasso')
|
||||||
|
->getRelation($mietinkasso, 'aIKnowledge')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($aiKnowledge) {
|
||||||
|
$this->relateDocument($aiKnowledge, 'dokumentes', $entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CAdvowareAkten PropagateDocumentsUp Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Verknüpfe Dokument (nur wenn nicht bereits verknüpft)
|
||||||
|
*/
|
||||||
|
private function relateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob bereits verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if (!$isRelated) {
|
||||||
|
$relation->relate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,57 +3,68 @@
|
|||||||
namespace Espo\Custom\Hooks\CDokumente;
|
namespace Espo\Custom\Hooks\CDokumente;
|
||||||
|
|
||||||
use Espo\ORM\Entity;
|
use Espo\ORM\Entity;
|
||||||
use Espo\Core\Utils\Util;
|
use Espo\ORM\Repository\Option\SaveOptions;
|
||||||
|
use Espo\Core\Hook\Hook\BeforeSave;
|
||||||
|
|
||||||
class CDokumente extends \Espo\Core\Hooks\Base
|
/**
|
||||||
|
* Hook: Berechnet Dokumenten-Hashes und setzt fileStatus
|
||||||
|
*
|
||||||
|
* Verwendet Blake3 als Hash-Algorithmus für optimale Performance
|
||||||
|
*/
|
||||||
|
class CDokumente implements BeforeSave
|
||||||
{
|
{
|
||||||
public function beforeSave(Entity $entity, array $options = [])
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function beforeSave(Entity $entity, SaveOptions $options): void
|
||||||
{
|
{
|
||||||
// Nur berechnen, wenn die Datei geändert wurde oder neu
|
// Problem: isAttributeChanged('dokument') erkennt Datei-Änderungen nicht,
|
||||||
if (!$entity->isNew() && !$entity->isAttributeChanged('dokument')) {
|
// da EspoCRM Datei-Uploads nicht als Feld-Änderung markiert.
|
||||||
|
// Daher läuft der Hook bei jeder beforeSave mit dokument-Feld,
|
||||||
|
// um sicherzustellen, dass Hashes bei Datei-Änderungen berechnet werden.
|
||||||
|
// Optimierung wäre wünschenswert, aber nicht möglich mit aktueller API.
|
||||||
|
|
||||||
|
$dokumentId = $entity->get('dokumentId');
|
||||||
|
|
||||||
|
if (!$dokumentId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$dokument = $entity->get('dokument');
|
// Verwende EntityManager zur korrekten Relation-Verwaltung
|
||||||
|
$attachment = $this->entityManager->getEntityById('Attachment', $dokumentId);
|
||||||
if (!$dokument) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_object($dokument)) {
|
|
||||||
$attachment = $dokument;
|
|
||||||
} else {
|
|
||||||
$attachment = $this->getEntityManager()->getEntity('Attachment', $dokument);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$attachment) {
|
if (!$attachment) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$filePath = 'data/upload/' . $attachment->get('id');
|
$filePath = 'data/upload/' . $attachment->getId();
|
||||||
if (!file_exists($filePath)) {
|
if (!file_exists($filePath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Berechne neue Hashes
|
// Berechne Blake3 Hash
|
||||||
$newMd5 = hash_file('md5', $filePath);
|
$fileContent = file_get_contents($filePath);
|
||||||
$newSha256 = hash_file('sha256', $filePath);
|
if ($fileContent === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blake3 Hashing - schneller als SHA3 und kryptographisch sicher
|
||||||
|
$newBlake3 = blake3($fileContent);
|
||||||
|
|
||||||
// Setze Hashes
|
// Setze Hash
|
||||||
$entity->set('md5sum', $newMd5);
|
$entity->set('blake3hash', $newBlake3);
|
||||||
$entity->set('sha256', $newSha256);
|
|
||||||
|
|
||||||
// Bestimme Status
|
// Bestimme Status
|
||||||
if ($entity->isNew()) {
|
if ($entity->isNew()) {
|
||||||
$entity->set('fileStatus', 'new');
|
$entity->set('fileStatus', 'new');
|
||||||
} else {
|
} else {
|
||||||
$oldMd5 = $entity->getFetched('md5sum');
|
$oldBlake3 = $entity->getFetched('blake3hash');
|
||||||
$oldSha256 = $entity->getFetched('sha256');
|
|
||||||
|
|
||||||
if ($oldMd5 !== $newMd5 || $oldSha256 !== $newSha256) {
|
if ($oldBlake3 !== $newBlake3) {
|
||||||
$entity->set('fileStatus', 'changed');
|
$entity->set('fileStatus', 'changed');
|
||||||
} else {
|
} else {
|
||||||
$entity->set('fileStatus', 'unchanged');
|
$entity->set('fileStatus', 'synced');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
122
custom/Espo/Custom/Hooks/CDokumente/UpdateJunctionSyncStatus.php
Normal file
122
custom/Espo/Custom/Hooks/CDokumente/UpdateJunctionSyncStatus.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CDokumente;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\ORM\Repository\Option\SaveOptions;
|
||||||
|
use Espo\Core\Hook\Hook\AfterSave;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Bei Änderung eines Dokuments wird syncStatus auf "unclean" gesetzt
|
||||||
|
* und alle verknüpften AIKnowledge Junction-Table-Einträge werden aktualisiert
|
||||||
|
*/
|
||||||
|
class UpdateJunctionSyncStatus implements AfterSave
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterSave(Entity $entity, SaveOptions $options): void
|
||||||
|
{
|
||||||
|
// Überspringe bei Create (nur bei Update)
|
||||||
|
if ($entity->isNew()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Überspringe, wenn keine relevanten Felder geändert wurden
|
||||||
|
if (!$this->hasRelevantChanges($entity)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Set syncStatus = 'unclean' directly on CDokumente entity
|
||||||
|
// (only if it has an AdvowareAkte linked)
|
||||||
|
if ($entity->get('cAdvowareAktenId')) {
|
||||||
|
$entity->set('syncStatus', 'unclean');
|
||||||
|
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
|
||||||
|
// Also update the parent AdvowareAkte
|
||||||
|
$akte = $this->entityManager->getEntity('CAdvowareAkten', $entity->get('cAdvowareAktenId'));
|
||||||
|
if ($akte) {
|
||||||
|
$akte->set('syncStatus', 'unclean');
|
||||||
|
$this->entityManager->saveEntity($akte, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update AIKnowledge Junction-Tables (unchanged)
|
||||||
|
$this->updateAIKnowledgeJunctions($entity);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Fehler loggen, aber nicht werfen (um Save nicht zu blockieren)
|
||||||
|
$GLOBALS['log']->error('CDokumente UpdateJunctionSyncStatus Hook Error: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob relevante Felder geändert wurden
|
||||||
|
*/
|
||||||
|
private function hasRelevantChanges(Entity $entity): bool
|
||||||
|
{
|
||||||
|
// Relevante Felder für Sync-Status
|
||||||
|
$relevantFields = [
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'dokument',
|
||||||
|
'dokumentId',
|
||||||
|
'preview',
|
||||||
|
'previewId',
|
||||||
|
'fileStatus'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($relevantFields as $field) {
|
||||||
|
if ($entity->isAttributeChanged($field)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update AIKnowledge Junction-Tables
|
||||||
|
*/
|
||||||
|
private function updateAIKnowledgeJunctions(Entity $entity): void
|
||||||
|
{
|
||||||
|
$updateQuery = $this->entityManager->getQueryBuilder()
|
||||||
|
->update()
|
||||||
|
->in('CAIKnowledgeDokumente')
|
||||||
|
->set(['syncstatus' => 'unclean'])
|
||||||
|
->where([
|
||||||
|
'cDokumenteId' => $entity->getId(),
|
||||||
|
'deleted' => false
|
||||||
|
])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$this->entityManager->getQueryExecutor()->execute($updateQuery);
|
||||||
|
|
||||||
|
// Hole alle betroffenen AIKnowledge IDs
|
||||||
|
$selectQuery = $this->entityManager->getQueryBuilder()
|
||||||
|
->select(['cAIKnowledgeId'])
|
||||||
|
->from('CAIKnowledgeDokumente')
|
||||||
|
->where([
|
||||||
|
'cDokumenteId' => $entity->getId(),
|
||||||
|
'deleted' => false
|
||||||
|
])
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$pdoStatement = $this->entityManager->getQueryExecutor()->execute($selectQuery);
|
||||||
|
$rows = $pdoStatement->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Trigger Update auf jeder AIKnowledge (um CheckGlobalSyncStatus Hook auszulösen)
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$knowledgeId = $row['cAIKnowledgeId'] ?? null;
|
||||||
|
if ($knowledgeId) {
|
||||||
|
$knowledge = $this->entityManager->getEntity('CAIKnowledge', $knowledgeId);
|
||||||
|
if ($knowledge) {
|
||||||
|
// Force Update ohne Hook-Loop
|
||||||
|
$knowledge->set('syncStatus', 'unclean');
|
||||||
|
$this->entityManager->saveEntity($knowledge, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
custom/Espo/Custom/Hooks/CKuendigung/CreateAdvowareAkte.php
Normal file
134
custom/Espo/Custom/Hooks/CKuendigung/CreateAdvowareAkte.php
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CKuendigung;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterSave;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Erstellt automatisch AdvowareAkte für Kündigung
|
||||||
|
*
|
||||||
|
* Wenn eine Kündigung erstellt/gespeichert wird:
|
||||||
|
* - Prüfe ob bereits eine AdvowareAkte vorhanden ist (über verknüpfte Räumungsklage)
|
||||||
|
* - Wenn nein: Erstelle neue AdvowareAkte und verknüpfe sie
|
||||||
|
*/
|
||||||
|
class CreateAdvowareAkte implements AfterSave
|
||||||
|
{
|
||||||
|
private static array $processing = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager,
|
||||||
|
private \Espo\Core\InjectableFactory $injectableFactory
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterSave(
|
||||||
|
Entity $entity,
|
||||||
|
\Espo\ORM\Repository\Option\SaveOptions $options
|
||||||
|
): void {
|
||||||
|
// Skip if silent or during hooks
|
||||||
|
if ($options->get('silent') || $options->get('skipHooks')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-create-akte';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prüfe ob Kündigung bereits eine AdvowareAkte hat
|
||||||
|
$existingAkteId = $entity->get('advowareAktenId');
|
||||||
|
|
||||||
|
if ($existingAkteId) {
|
||||||
|
$GLOBALS['log']->info("CKuendigung CreateAdvowareAkte: Kündigung already has AdvowareAkte: {$existingAkteId}");
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
return; // Bereits vorhanden
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob verknüpfte Räumungsklagen eine Akte haben
|
||||||
|
$raeumungsklagen = $this->entityManager
|
||||||
|
->getRDBRepository('CKuendigung')
|
||||||
|
->getRelation($entity, 'vmhRumungsklages')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
foreach ($raeumungsklagen as $rk) {
|
||||||
|
$rkAkteId = $rk->get('advowareAktenId');
|
||||||
|
if ($rkAkteId) {
|
||||||
|
// Übernehme Akte von Räumungsklage
|
||||||
|
$entity->set('advowareAktenId', $rkAkteId);
|
||||||
|
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
|
||||||
|
// Synchronisiere Aktennummer
|
||||||
|
$akte = $this->entityManager->getEntity('CAdvowareAkten', $rkAkteId);
|
||||||
|
if ($akte) {
|
||||||
|
$this->syncAktennummer($entity, $akte);
|
||||||
|
}
|
||||||
|
|
||||||
|
$GLOBALS['log']->info("CKuendigung CreateAdvowareAkte: Using AdvowareAkte from Räumungsklage: {$rkAkteId}");
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keine Akte gefunden -> Erstelle neue
|
||||||
|
$this->createNewAkte($entity);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CKuendigung CreateAdvowareAkte Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createNewAkte(Entity $kuendigung): void
|
||||||
|
{
|
||||||
|
// Hole Aktennummer aus Kündigung (falls vorhanden)
|
||||||
|
$aktennummer = $kuendigung->get('aktennr');
|
||||||
|
if (!$aktennummer) {
|
||||||
|
$aktennummer = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erstelle AdvowareAkte (aktenzeichen bleibt leer)
|
||||||
|
$akteData = [
|
||||||
|
'name' => 'Advoware Akte - ' . $kuendigung->get('name'),
|
||||||
|
'aktennummer' => $aktennummer,
|
||||||
|
'syncStatus' => 'unclean',
|
||||||
|
'assignedUserId' => $kuendigung->get('assignedUserId')
|
||||||
|
];
|
||||||
|
|
||||||
|
// Copy teams
|
||||||
|
$teamsIds = $kuendigung->getLinkMultipleIdList('teams');
|
||||||
|
if (!empty($teamsIds)) {
|
||||||
|
$akteData['teamsIds'] = $teamsIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
$akte = $this->entityManager->createEntity('CAdvowareAkten', $akteData);
|
||||||
|
|
||||||
|
if ($akte) {
|
||||||
|
// Verknüpfe mit Kündigung
|
||||||
|
$kuendigung->set('advowareAktenId', $akte->getId());
|
||||||
|
$this->entityManager->saveEntity($kuendigung, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
|
||||||
|
// Synchronisiere Aktennummer zurück zur Kündigung (falls leer war)
|
||||||
|
if (!$kuendigung->get('aktennr') && $akte->get('aktennummer')) {
|
||||||
|
$kuendigung->set('aktennr', $akte->get('aktennummer'));
|
||||||
|
$this->entityManager->saveEntity($kuendigung, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$GLOBALS['log']->info("CKuendigung CreateAdvowareAkte: Created new AdvowareAkte: {$akte->getId()}");
|
||||||
|
} else {
|
||||||
|
$GLOBALS['log']->error('CKuendigung CreateAdvowareAkte: Failed to create AdvowareAkte');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncAktennummer(Entity $kuendigung, Entity $akte): void
|
||||||
|
{
|
||||||
|
// Synchronisiere nur Aktennummer (nicht Aktenzeichen, das ist in der Beziehung)
|
||||||
|
if (!$kuendigung->get('aktennr') && $akte->get('aktennummer')) {
|
||||||
|
$kuendigung->set('aktennr', $akte->get('aktennummer'));
|
||||||
|
$this->entityManager->saveEntity($kuendigung, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
$GLOBALS['log']->info("CKuendigung CreateAdvowareAkte: Synchronized Aktennummer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
custom/Espo/Custom/Hooks/CKuendigung/SyncAdvowareAkte.php
Normal file
84
custom/Espo/Custom/Hooks/CKuendigung/SyncAdvowareAkte.php
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CKuendigung;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterRelate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Synchronisiert AdvowareAkte zwischen Kündigung und Räumungsklage
|
||||||
|
*
|
||||||
|
* Wenn eine Kündigung mit einer Räumungsklage verknüpft wird:
|
||||||
|
* - Prüfe ob Räumungsklage eine AdvowareAkte hat
|
||||||
|
* - Wenn ja, verknüpfe diese Akte auch mit der Kündigung
|
||||||
|
* - Übernehme/Synchronisiere Aktennummer und Aktenzeichen
|
||||||
|
*/
|
||||||
|
class SyncAdvowareAkte implements AfterRelate
|
||||||
|
{
|
||||||
|
private static array $processing = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterRelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
array $columnData,
|
||||||
|
\Espo\ORM\Repository\Option\RelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für vmhRumungsklages-Beziehung (wenn Räumungsklage zu Kündigung hinzugefügt wird)
|
||||||
|
if ($relationName !== 'vmhRumungsklages') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-sync-akte';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// $entity = CKuendigung
|
||||||
|
// $foreignEntity = CVmhRumungsklage
|
||||||
|
|
||||||
|
// Hole AdvowareAkte von der Räumungsklage (hasOne relationship - get via field)
|
||||||
|
$advowareAkteId = $foreignEntity->get('advowareAktenId');
|
||||||
|
|
||||||
|
if ($advowareAkteId) {
|
||||||
|
$advowareAkte = $this->entityManager->getEntity('CAdvowareAkten', $advowareAkteId);
|
||||||
|
|
||||||
|
if ($advowareAkte) {
|
||||||
|
$GLOBALS['log']->info("CKuendigung SyncAdvowareAkte: Found AdvowareAkte {$advowareAkte->getId()} on Räumungsklage {$foreignEntity->getId()}");
|
||||||
|
|
||||||
|
// Prüfe ob Kündigung bereits eine andere Akte hat
|
||||||
|
$existingAktenId = $entity->get('advowareAktenId');
|
||||||
|
|
||||||
|
if ($existingAktenId && $existingAktenId !== $advowareAkteId) {
|
||||||
|
$GLOBALS['log']->warning("CKuendigung SyncAdvowareAkte: Kündigung already has different AdvowareAkte {$existingAktenId}, will replace with {$advowareAkteId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verknüpfe AdvowareAkte mit Kündigung (belongsTo relationship - set field directly)
|
||||||
|
$entity->set('advowareAktenId', $advowareAkteId);
|
||||||
|
|
||||||
|
// Synchronisiere nur Aktennummer (Aktenzeichen kommt über Beziehung)
|
||||||
|
if (!$entity->get('aktennr') && $advowareAkte->get('aktennummer')) {
|
||||||
|
$entity->set('aktennr', $advowareAkte->get('aktennummer'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save once with all changes
|
||||||
|
$this->entityManager->saveEntity($entity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
$GLOBALS['log']->info("CKuendigung SyncAdvowareAkte: Successfully linked AdvowareAkte and synchronized Aktennummer to Kündigung");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$GLOBALS['log']->info("CKuendigung SyncAdvowareAkte: Räumungsklage {$foreignEntity->getId()} has no AdvowareAkte yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CKuendigung SyncAdvowareAkte Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
custom/Espo/Custom/Hooks/CMietinkasso/PropagateDocuments.php
Normal file
157
custom/Espo/Custom/Hooks/CMietinkasso/PropagateDocuments.php
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CMietinkasso;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterRelate;
|
||||||
|
use Espo\Core\Hook\Hook\AfterUnrelate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Propagiert Dokumenten-Verknüpfungen von Mietinkasso zu AdvowareAkten und AIKnowledge
|
||||||
|
*
|
||||||
|
* - Wenn Dokument mit Mietinkasso verknüpft wird → verknüpfe auch mit AdvowareAkten + AIKnowledge
|
||||||
|
* - Wenn Dokument von Mietinkasso entknüpft wird → entknüpfe auch von AdvowareAkten + AIKnowledge
|
||||||
|
*/
|
||||||
|
class PropagateDocuments implements AfterRelate, AfterUnrelate
|
||||||
|
{
|
||||||
|
private static array $processing = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterRelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
array $columnData,
|
||||||
|
\Espo\ORM\Repository\Option\RelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentesmietinkasso-Beziehung
|
||||||
|
if ($relationName !== 'dokumentesmietinkasso') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-relate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hole verbundene AdvowareAkten
|
||||||
|
$advowareAkten = $this->entityManager
|
||||||
|
->getRDBRepository('CMietinkasso')
|
||||||
|
->getRelation($entity, 'advowareAkten')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Set direct belongsTo relationship on document
|
||||||
|
if ($advowareAkten) {
|
||||||
|
$foreignEntity->set('cAdvowareAktenId', $advowareAkten->getId());
|
||||||
|
$foreignEntity->set('syncStatus', 'new'); // Mark as new for Advoware sync
|
||||||
|
$this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hole verbundene AIKnowledge
|
||||||
|
$aIKnowledge = $this->entityManager
|
||||||
|
->getRDBRepository('CMietinkasso')
|
||||||
|
->getRelation($entity, 'aIKnowledge')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Verknüpfe Dokument mit AIKnowledge
|
||||||
|
if ($aIKnowledge) {
|
||||||
|
$this->relateDocument($aIKnowledge, 'dokumentes', $foreignEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CMietinkasso PropagateDocuments (relate) Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function afterUnrelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
\Espo\ORM\Repository\Option\UnrelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentesmietinkasso-Beziehung
|
||||||
|
if ($relationName !== 'dokumentesmietinkasso') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-unrelate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hole verbundene AdvowareAkten
|
||||||
|
$advowareAkten = $this->entityManager
|
||||||
|
->getRDBRepository('CMietinkasso')
|
||||||
|
->getRelation($entity, 'advowareAkten')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Remove direct belongsTo relationship from document
|
||||||
|
if ($advowareAkten && $foreignEntity->get('cAdvowareAktenId') === $advowareAkten->getId()) {
|
||||||
|
$foreignEntity->set('cAdvowareAktenId', null);
|
||||||
|
$this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hole verbundene AIKnowledge
|
||||||
|
$aIKnowledge = $this->entityManager
|
||||||
|
->getRDBRepository('CMietinkasso')
|
||||||
|
->getRelation($entity, 'aIKnowledge')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Entknüpfe Dokument von AIKnowledge
|
||||||
|
if ($aIKnowledge) {
|
||||||
|
$this->unrelateDocument($aIKnowledge, 'dokumentes', $foreignEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CMietinkasso PropagateDocuments (unrelate) Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Verknüpfe Dokument (nur wenn nicht bereits verknüpft)
|
||||||
|
*/
|
||||||
|
private function relateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob bereits verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if (!$isRelated) {
|
||||||
|
$relation->relate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Entknüpfe Dokument
|
||||||
|
*/
|
||||||
|
private function unrelateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($isRelated) {
|
||||||
|
$relation->unrelate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
custom/Espo/Custom/Hooks/CVmhRumungsklage/PropagateDocuments.php
Normal file
157
custom/Espo/Custom/Hooks/CVmhRumungsklage/PropagateDocuments.php
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Hooks\CVmhRumungsklage;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Hook\Hook\AfterRelate;
|
||||||
|
use Espo\Core\Hook\Hook\AfterUnrelate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Propagiert Dokumenten-Verknüpfungen von Räumungsklage zu AdvowareAkten und AIKnowledge
|
||||||
|
*
|
||||||
|
* - Wenn Dokument mit Räumungsklage verknüpft wird → verknüpfe auch mit AdvowareAkten + AIKnowledge
|
||||||
|
* - Wenn Dokument von Räumungsklage entknüpft wird → entknüpfe auch von AdvowareAkten + AIKnowledge
|
||||||
|
*/
|
||||||
|
class PropagateDocuments implements AfterRelate, AfterUnrelate
|
||||||
|
{
|
||||||
|
private static array $processing = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private \Espo\ORM\EntityManager $entityManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function afterRelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
array $columnData,
|
||||||
|
\Espo\ORM\Repository\Option\RelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentesvmhraumungsklage-Beziehung
|
||||||
|
if ($relationName !== 'dokumentesvmhraumungsklage') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-relate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hole verbundene AdvowareAkten
|
||||||
|
$advowareAkten = $this->entityManager
|
||||||
|
->getRDBRepository('CVmhRumungsklage')
|
||||||
|
->getRelation($entity, 'advowareAkten')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Set direct belongsTo relationship on document
|
||||||
|
if ($advowareAkten) {
|
||||||
|
$foreignEntity->set('cAdvowareAktenId', $advowareAkten->getId());
|
||||||
|
$foreignEntity->set('syncStatus', 'new'); // Mark as new for Advoware sync
|
||||||
|
$this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hole verbundene AIKnowledge
|
||||||
|
$aIKnowledge = $this->entityManager
|
||||||
|
->getRDBRepository('CVmhRumungsklage')
|
||||||
|
->getRelation($entity, 'aIKnowledge')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Verknüpfe Dokument mit AIKnowledge
|
||||||
|
if ($aIKnowledge) {
|
||||||
|
$this->relateDocument($aIKnowledge, 'dokumentes', $foreignEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CVmhRumungsklage PropagateDocuments (relate) Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function afterUnrelate(
|
||||||
|
Entity $entity,
|
||||||
|
string $relationName,
|
||||||
|
Entity $foreignEntity,
|
||||||
|
\Espo\ORM\Repository\Option\UnrelateOptions $options
|
||||||
|
): void {
|
||||||
|
// Nur für dokumentesvmhraumungsklage-Beziehung
|
||||||
|
if ($relationName !== 'dokumentesvmhraumungsklage') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vermeide Loops
|
||||||
|
$key = $entity->getId() . '-' . $foreignEntity->getId() . '-unrelate';
|
||||||
|
if (isset(self::$processing[$key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$processing[$key] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hole verbundene AdvowareAkten
|
||||||
|
$advowareAkten = $this->entityManager
|
||||||
|
->getRDBRepository('CVmhRumungsklage')
|
||||||
|
->getRelation($entity, 'advowareAkten')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Remove direct belongsTo relationship from document
|
||||||
|
if ($advowareAkten && $foreignEntity->get('cAdvowareAktenId') === $advowareAkten->getId()) {
|
||||||
|
$foreignEntity->set('cAdvowareAktenId', null);
|
||||||
|
$this->entityManager->saveEntity($foreignEntity, ['silent' => true, 'skipHooks' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hole verbundene AIKnowledge
|
||||||
|
$aIKnowledge = $this->entityManager
|
||||||
|
->getRDBRepository('CVmhRumungsklage')
|
||||||
|
->getRelation($entity, 'aIKnowledge')
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
// Entknüpfe Dokument von AIKnowledge
|
||||||
|
if ($aIKnowledge) {
|
||||||
|
$this->unrelateDocument($aIKnowledge, 'dokumentes', $foreignEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$GLOBALS['log']->error('CVmhRumungsklage PropagateDocuments (unrelate) Error: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
unset(self::$processing[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Verknüpfe Dokument (nur wenn nicht bereits verknüpft)
|
||||||
|
*/
|
||||||
|
private function relateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob bereits verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if (!$isRelated) {
|
||||||
|
$relation->relate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion: Entknüpfe Dokument
|
||||||
|
*/
|
||||||
|
private function unrelateDocument(Entity $parentEntity, string $relationName, Entity $document): void
|
||||||
|
{
|
||||||
|
$repository = $this->entityManager->getRDBRepository($parentEntity->getEntityType());
|
||||||
|
$relation = $repository->getRelation($parentEntity, $relationName);
|
||||||
|
|
||||||
|
// Prüfe ob verknüpft
|
||||||
|
$isRelated = $relation
|
||||||
|
->where(['id' => $document->getId()])
|
||||||
|
->findOne();
|
||||||
|
|
||||||
|
if ($isRelated) {
|
||||||
|
$relation->unrelate($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "إنشاء {الكيانTypeTranslated}"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/ar_AR/CAdvowareAkten.json
Normal file
10
custom/Espo/Custom/Resources/i18n/ar_AR/CAdvowareAkten.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "الاجتماعات",
|
||||||
|
"calls": "المكالمات",
|
||||||
|
"tasks": "مهام"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAdvowareAkten": "إنشاء {الكيانTypeTranslated}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "Създаване на AI Knowledge"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/bg_BG/CAdvowareAkten.json
Normal file
10
custom/Espo/Custom/Resources/i18n/bg_BG/CAdvowareAkten.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Срещи",
|
||||||
|
"calls": "Разговори",
|
||||||
|
"tasks": "Задачи"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAdvowareAkten": "Създаване на Advoware Akten"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "Vytvořit AI Knowledge"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/cs_CZ/CAdvowareAkten.json
Normal file
10
custom/Espo/Custom/Resources/i18n/cs_CZ/CAdvowareAkten.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Schůzky",
|
||||||
|
"calls": "Hovory",
|
||||||
|
"tasks": "Úkoly"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAdvowareAkten": "Vytvořit Advoware Akten"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "Opret AI Knowledge "
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/da_DK/CAdvowareAkten.json
Normal file
10
custom/Espo/Custom/Resources/i18n/da_DK/CAdvowareAkten.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Møder",
|
||||||
|
"calls": "Opkald",
|
||||||
|
"tasks": "Opgaver"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAdvowareAkten": "Opret Advoware Akten "
|
||||||
|
}
|
||||||
|
}
|
||||||
49
custom/Espo/Custom/Resources/i18n/de_DE/CAIKnowledge.json
Normal file
49
custom/Espo/Custom/Resources/i18n/de_DE/CAIKnowledge.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "AI Knowledge erstellen"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"dokumentes": "Dokumente",
|
||||||
|
"vmhRumungsklage": "Räumungsklage",
|
||||||
|
"mietinkasso": "Mietinkasso",
|
||||||
|
"datenbankId": "Datenbank-ID",
|
||||||
|
"syncStatus": "Sync-Status",
|
||||||
|
"lastSync": "Letzte Synchronisation",
|
||||||
|
"aktivierungsstatus": "Aktivierungsstatus",
|
||||||
|
"dokumenteAiDocumentId": "AI Document ID",
|
||||||
|
"dokumenteSyncstatus": "Sync-Status",
|
||||||
|
"dokumenteLastSync": "Letzter Sync",
|
||||||
|
"dokumenteSyncedHash": "Sync-Hash"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"dokumentes": "Dokumente",
|
||||||
|
"vmhRumungsklage": "Räumungsklage",
|
||||||
|
"mietinkasso": "Mietinkasso"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"syncStatus": {
|
||||||
|
"synced": "Synchronisiert",
|
||||||
|
"unclean": "Nicht synchronisiert",
|
||||||
|
"pending_sync": "Synchronisierung ausstehend"
|
||||||
|
},
|
||||||
|
"aktivierungsstatus": {
|
||||||
|
"new": "Neu",
|
||||||
|
"active": "Aktiv",
|
||||||
|
"paused": "Pausiert",
|
||||||
|
"deactivated": "Deaktiviert"
|
||||||
|
},
|
||||||
|
"dokumenteSyncstatus": {
|
||||||
|
"new": "Neu",
|
||||||
|
"unclean": "Nicht synchronisiert",
|
||||||
|
"synced": "Synchronisiert",
|
||||||
|
"failed": "Fehlgeschlagen",
|
||||||
|
"unsupported": "Nicht unterstützt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"syncStatus": "Globaler Synchronisationsstatus: synced = Alle Dokumente synchronisiert, unclean = Mindestens ein Dokument ist neu oder hat Änderungen, pending_sync = Synchronisierung wurde gestartet aber noch nicht abgeschlossen. Wird automatisch basierend auf den Dokumenten-Status aktualisiert.",
|
||||||
|
"lastSync": "Zeitpunkt der letzten erfolgreichen Synchronisation aller Dokumente",
|
||||||
|
"aktivierungsstatus": "Aktivierungsstatus des AI Knowledge Entries: new = Neu angelegt, active = Aktiv synchronisiert, paused = Synchronisation pausiert, deactivated = Synchronisation deaktiviert",
|
||||||
|
"datenbankId": "Eindeutige ID in der AI-Datenbank"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"cAIKnowledge": "AI Knowledge",
|
||||||
|
"cDokumente": "Dokument",
|
||||||
|
"aiDocumentId": "AI Dokument-ID",
|
||||||
|
"syncstatus": "Sync-Status",
|
||||||
|
"lastSync": "Letzte Synchronisation",
|
||||||
|
"syncedHash": "Sync-Hash"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"cAIKnowledge": "AI Knowledge",
|
||||||
|
"cDokumente": "Dokument"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledgeCDokumente": "Verknüpfung erstellen"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"syncstatus": {
|
||||||
|
"new": "Neu",
|
||||||
|
"unclean": "Geändert",
|
||||||
|
"synced": "Synchronisiert",
|
||||||
|
"failed": "Fehler",
|
||||||
|
"unsupported": "Nicht unterstützt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"aiDocumentId": "Externe AI-Dokument-Referenz-ID",
|
||||||
|
"syncedHash": "Hash-Wert des zuletzt synchronisierten Dokument-Zustands"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"cAIKnowledge": "AI Knowledge",
|
||||||
|
"cDokumente": "Dokument",
|
||||||
|
"aiDocumentId": "AI Dokument-ID",
|
||||||
|
"syncstatus": "Sync-Status",
|
||||||
|
"lastSync": "Letzte Synchronisation",
|
||||||
|
"syncedHash": "Sync-Hash"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"cAIKnowledge": "AI Knowledge",
|
||||||
|
"cDokumente": "Dokument"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledgeCDokumente": "Verknüpfung erstellen"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"syncstatus": {
|
||||||
|
"new": "Neu",
|
||||||
|
"unclean": "Geändert",
|
||||||
|
"synced": "Synchronisiert",
|
||||||
|
"failed": "Fehler",
|
||||||
|
"unsupported": "Nicht unterstützt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"aiDocumentId": "Externe AI-Dokument-Referenz-ID",
|
||||||
|
"syncedHash": "Hash-Wert des zuletzt synchronisierten Dokument-Zustands"
|
||||||
|
}
|
||||||
|
}
|
||||||
47
custom/Espo/Custom/Resources/i18n/de_DE/CAdvowareAkten.json
Normal file
47
custom/Espo/Custom/Resources/i18n/de_DE/CAdvowareAkten.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"calls": "Anrufe",
|
||||||
|
"tasks": "Aufgaben",
|
||||||
|
"vmhRumungsklage": "Räumungsklagen",
|
||||||
|
"mietinkasso": "Mietinkasso",
|
||||||
|
"kuendigungen": "Kündigungen",
|
||||||
|
"dokumentes": "Dokumente"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAdvowareAkten": "Advoware Akten erstellen"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"vmhRumungsklage": "Räumungsklagen",
|
||||||
|
"mietinkasso": "Mietinkasso",
|
||||||
|
"aktenzeichen": "Aktenzeichen",
|
||||||
|
"aktennummer": "Aktennummer",
|
||||||
|
"syncStatus": "Sync-Status",
|
||||||
|
"lastSync": "Letzte Synchronisation",
|
||||||
|
"aktivierungsstatus": "Aktivierungsstatus",
|
||||||
|
"dokumentes": "Dokumente",
|
||||||
|
"dokumenteHnr": "HNR",
|
||||||
|
"dokumenteSyncstatus": "Sync-Status",
|
||||||
|
"dokumenteLastSync": "Letzter Sync",
|
||||||
|
"dokumenteSyncedHash": "Sync-Hash"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"syncStatus": {
|
||||||
|
"synced": "Synchronisiert",
|
||||||
|
"unclean": "Nicht synchronisiert",
|
||||||
|
"pending_sync": "Synchronisierung ausstehend",
|
||||||
|
"failed": "Fehlgeschlagen"
|
||||||
|
},
|
||||||
|
"aktivierungsstatus": {
|
||||||
|
"new": "Neu",
|
||||||
|
"import": "Import",
|
||||||
|
"active": "Aktiv",
|
||||||
|
"paused": "Pausiert",
|
||||||
|
"deactivated": "Deaktiviert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"syncStatus": "Globaler Synchronisationsstatus: synced = Alle Dokumente synchronisiert, unclean = Mindestens ein Dokument ist neu oder hat Änderungen, pending_sync = Synchronisierung wurde gestartet aber noch nicht abgeschlossen, failed = Synchronisierung fehlgeschlagen. Wird automatisch basierend auf den Dokumenten-Status aktualisiert.",
|
||||||
|
"lastSync": "Zeitpunkt der letzten erfolgreichen Synchronisation aller Dokumente",
|
||||||
|
"aktivierungsstatus": "Aktivierungsstatus der Akte: new = Neu angelegt, import = Aus Advoware importiert, active = Aktiv synchronisiert, paused = Synchronisation pausiert, deactivated = Synchronisation deaktiviert"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAdvowareAktenCDokumente": "Advoware-Dokument-Verknüpfung erstellen",
|
||||||
|
"CAdvowareAktenCDokumente": "Advoware-Dokument-Verknüpfungen"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"cAdvowareAkten": "Advoware-Akte",
|
||||||
|
"cAdvowareAktenId": "Advoware-Akte ID",
|
||||||
|
"cDokumente": "Dokument",
|
||||||
|
"cDokumenteId": "Dokument ID",
|
||||||
|
"hnr": "HNR",
|
||||||
|
"syncStatus": "Sync-Status",
|
||||||
|
"syncedHash": "Sync-Hash",
|
||||||
|
"deleted": "Gelöscht"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"cAdvowareAkten": "Advoware-Akte",
|
||||||
|
"cDokumente": "Dokument"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"syncStatus": {
|
||||||
|
"new": "Neu",
|
||||||
|
"changed": "Geändert",
|
||||||
|
"synced": "Synchronisiert",
|
||||||
|
"deleted": "Gelöscht"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"hnr": "Advoware HNR Referenz für dieses Dokument",
|
||||||
|
"syncStatus": "Synchronisierungsstatus mit Advoware",
|
||||||
|
"syncedHash": "Hash-Wert des zuletzt synchronisierten Dokument-Zustands (zur Änderungserkennung)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,67 +1,67 @@
|
|||||||
{
|
{
|
||||||
"fields": {
|
"fields": {
|
||||||
"dokument": "Download",
|
"dokument": "Download",
|
||||||
"preview": "Vorschau",
|
"preview": "Vorschau",
|
||||||
"ydocumentuuid": "Y-Document-UUID",
|
"blake3hash": "Blake3-Hash",
|
||||||
"md5sum": "MD5-Prüfsumme",
|
"cAdvowareAkten": "Advoware Akte",
|
||||||
"sha256": "SHA256-Prüfsumme",
|
"cAdvowareAktenId": "Advoware Akten-ID",
|
||||||
"aktennr": "Advoware Identifikator",
|
"cAdvowareAktenName": "Advoware Aktenname",
|
||||||
"advowareLastSync": "Advoware letzte Synchronisation",
|
"hnr": "HNR (Advoware)",
|
||||||
"syncStatus": "Sync-Status",
|
"syncStatus": "Sync-Status",
|
||||||
"xaiId": "x.AI ID",
|
"syncedHash": "Sync-Hash",
|
||||||
"xaiCollections": "x.AI Collections",
|
"usn": "USN",
|
||||||
"xaiSyncStatus": "Sync-Status",
|
"dateipfad": "Dateipfad",
|
||||||
"fileStatus": "Datei-Status",
|
"lastSyncTimestamp": "Letzter Sync",
|
||||||
"contactsvmhdokumente": "Freigegebene Nutzer",
|
"advowareArt": "Advoware Art",
|
||||||
"vmhMietverhltnisesDokumente": "Mietverhältnisse",
|
"advowareBemerkung": "Advoware Bemerkung",
|
||||||
"vmhErstgespraechsdokumente": "Erstgespräche",
|
"contactsvmhdokumente": "Freigegebene Nutzer",
|
||||||
"vmhRumungsklagesdokumente": "Räumungsklagen",
|
"vmhMietverhltnisesDokumente": "Mietverhältnisse",
|
||||||
"kuendigungDokumente": "Kündigungen",
|
"vmhErstgespraechsdokumente": "Erstgespräche",
|
||||||
"beteiligte2dokumente": "Beteiligte",
|
"vmhRumungsklagesdokumente": "Räumungsklagen",
|
||||||
"mietobjekt2dokumente": "Mietobjekte",
|
"kuendigungDokumente": "Kündigungen",
|
||||||
"mietinkassosdokumente": "Mietinkasso",
|
"beteiligte2dokumente": "Beteiligte",
|
||||||
"kndigungensdokumente": "Kündigungen"
|
"mietobjekt2dokumente": "Mietobjekte",
|
||||||
},
|
"mietinkassosdokumente": "Mietinkasso",
|
||||||
"links": {
|
"kndigungensdokumente": "Kündigungen",
|
||||||
"contactsvmhdokumente": "Freigegebene Nutzer",
|
"aIKnowledges": "AI Knowledge",
|
||||||
"vmhMietverhltnisesDokumente": "Mietverhältnisse",
|
"aiKnowledgeAiDocumentId": "AI Document ID",
|
||||||
"vmhErstgespraechsdokumente": "Erstgespräche",
|
"aiKnowledgeSyncstatus": "AI Sync-Status",
|
||||||
"vmhRumungsklagesdokumente": "Räumungsklagen",
|
"aiKnowledgeLastSync": "AI Letzter Sync"
|
||||||
"kuendigungDokumente": "Kündigungen",
|
|
||||||
"beteiligte2dokumente": "Beteiligte",
|
|
||||||
"mietobjekt2dokumente": "Mietobjekte",
|
|
||||||
"mietinkassosdokumente": "Mietinkasso",
|
|
||||||
"kndigungensdokumente": "Kündigungen"
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"Create CDokumente": "Dokument erstellen"
|
|
||||||
},
|
|
||||||
"tooltips": {
|
|
||||||
"aktennr": "Eindeutige Dokument-Nummer aus Advoware",
|
|
||||||
"advowareLastSync": "Zeitpunkt der letzten Synchronisation mit Advoware",
|
|
||||||
"syncStatus": "Status der Synchronisation: pending_sync = Warte auf Sync, clean = erfolgreich, unclean = Abweichungen, failed = Fehler",
|
|
||||||
"xaiId": "Eindeutige ID für x.AI Synchronisation",
|
|
||||||
"xaiCollections": "Liste der x.AI Collections für dieses Dokument",
|
|
||||||
"xaiSyncStatus": "Status der x.AI Synchronisation: pending_sync = Warte auf Sync, clean = erfolgreich, unclean = Abweichungen, failed = Fehler",
|
|
||||||
"fileStatus": "Status der Datei: new = neu hochgeladen, changed = geändert, unchanged = unverändert"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"syncStatus": {
|
|
||||||
"pending_sync": "Warte auf Sync",
|
|
||||||
"clean": "Synchronisiert",
|
|
||||||
"unclean": "Abweichungen",
|
|
||||||
"failed": "Fehlgeschlagen"
|
|
||||||
},
|
},
|
||||||
"xaiSyncStatus": {
|
"links": {
|
||||||
"pending_sync": "Warte auf Sync",
|
"cAdvowareAkten": "Advoware Akte",
|
||||||
"clean": "Synchronisiert",
|
"contactsvmhdokumente": "Freigegebene Nutzer",
|
||||||
"unclean": "Abweichungen",
|
"vmhMietverhltnisesDokumente": "Mietverhältnisse",
|
||||||
"failed": "Fehlgeschlagen"
|
"vmhErstgespraechsdokumente": "Erstgespräche",
|
||||||
|
"vmhRumungsklagesdokumente": "Räumungsklagen",
|
||||||
|
"kuendigungDokumente": "Kündigungen",
|
||||||
|
"beteiligte2dokumente": "Beteiligte",
|
||||||
|
"mietobjekt2dokumente": "Mietobjekte",
|
||||||
|
"mietinkassosdokumente": "Mietinkasso",
|
||||||
|
"kndigungensdokumente": "Kündigungen",
|
||||||
|
"aIKnowledges": "AI Knowledge"
|
||||||
},
|
},
|
||||||
"fileStatus": {
|
"labels": {
|
||||||
"new": "Neu",
|
"Create CDokumente": "Dokument erstellen"
|
||||||
"changed": "Geändert",
|
},
|
||||||
"unchanged": "Unverändert"
|
"tooltips": {
|
||||||
|
"blake3hash": "Kryptografischer Blake3-Hash der Datei (schneller und sicherer als MD5/SHA256)",
|
||||||
|
"hnr": "Hierarchische Referenznummer in Advoware",
|
||||||
|
"syncStatus": "Status der Synchronisation mit Advoware: new=neu, unclean=geändert, synced=synchronisiert, failed=Fehler, unsupported=nicht unterstützt",
|
||||||
|
"syncedHash": "Hash-Wert bei letzter erfolgreicher Synchronisation",
|
||||||
|
"usn": "Update Sequence Number - Versionsnummer für Synchronisation",
|
||||||
|
"dateipfad": "Windows-Dateipfad des Dokuments in Advoware",
|
||||||
|
"lastSyncTimestamp": "Zeitstempel der letzten erfolgreichen Synchronisation mit Advoware",
|
||||||
|
"advowareArt": "Dokumententyp/Art wie in Advoware klassifiziert",
|
||||||
|
"advowareBemerkung": "Bemerkungsfeld aus Advoware - wird bei Sync übernommen"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"syncStatus": {
|
||||||
|
"new": "Neu",
|
||||||
|
"unclean": "Geändert",
|
||||||
|
"synced": "Synchronisiert",
|
||||||
|
"failed": "Fehler",
|
||||||
|
"unsupported": "Nicht unterstützt"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,10 @@
|
|||||||
"kuendiger": "Vermieter",
|
"kuendiger": "Vermieter",
|
||||||
"gekuendigte": "Mieter",
|
"gekuendigte": "Mieter",
|
||||||
"dokumenteskuendigung": "Dokumente",
|
"dokumenteskuendigung": "Dokumente",
|
||||||
"contactsKuendigung": "Portal-Freigaben"
|
"contactsKuendigung": "Portal-Freigaben",
|
||||||
|
"advowareAkten": "Advoware Akte",
|
||||||
|
"vmhRumungsklages": "Räumungsklagen",
|
||||||
|
"pulse": "Pulse"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CKuendigung": "Kündigung erstellen",
|
"Create CKuendigung": "Kündigung erstellen",
|
||||||
@@ -31,7 +34,7 @@
|
|||||||
"modifiedBy": "Geändert von",
|
"modifiedBy": "Geändert von",
|
||||||
"freigeschalteteNutzer": "Freigeschaltete Nutzer",
|
"freigeschalteteNutzer": "Freigeschaltete Nutzer",
|
||||||
"collaborators": "Mitarbeiter",
|
"collaborators": "Mitarbeiter",
|
||||||
"advowareAktenzeichen": "Advoware Aktenzeichen",
|
"advowareAkten": "Advoware Akte",
|
||||||
"aktennr": "Advoware Identifikator",
|
"aktennr": "Advoware Identifikator",
|
||||||
"advowareLastSync": "Letzter Sync",
|
"advowareLastSync": "Letzter Sync",
|
||||||
"syncStatus": "Sync Status",
|
"syncStatus": "Sync Status",
|
||||||
@@ -98,13 +101,13 @@
|
|||||||
"pending_sync": "Warte auf Sync",
|
"pending_sync": "Warte auf Sync",
|
||||||
"clean": "Synchronisiert",
|
"clean": "Synchronisiert",
|
||||||
"unclean": "Änderungen ausstehend",
|
"unclean": "Änderungen ausstehend",
|
||||||
"failed": "Fehler"
|
"failed": "Fehler",
|
||||||
|
"no_sync": "Kein Sync"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"advowareAktenzeichen": "Aktenzeichen aus Advoware für die Synchronisation",
|
|
||||||
"aktennr": "Eindeutige Kündigungs-Nummer aus Advoware",
|
"aktennr": "Eindeutige Kündigungs-Nummer aus Advoware",
|
||||||
"syncStatus": "Status der Synchronisation mit Advoware",
|
"syncStatus": "Status der Advoware-Synchronisation: pending_sync = Warte auf Sync, clean = erfolgreich synchronisiert, unclean = Änderungen ausstehend, failed = Fehler, no_sync = Nicht synchronisiert",
|
||||||
"sendungsverfolgungsnummer": "Sendungsverfolgungsnummer für Einschreiben",
|
"sendungsverfolgungsnummer": "Sendungsverfolgungsnummer für Einschreiben",
|
||||||
"mietrueckstand": "Gesamthöhe des Mietrückstands (nur bei Kündigungsgrund Mietrückstand)",
|
"mietrueckstand": "Gesamthöhe des Mietrückstands (nur bei Kündigungsgrund Mietrückstand)",
|
||||||
"mietkaution": "Einbehaltene oder ausstehende Mietkaution",
|
"mietkaution": "Einbehaltene oder ausstehende Mietkaution",
|
||||||
|
|||||||
@@ -1,52 +1,42 @@
|
|||||||
{
|
{
|
||||||
"fields": {
|
"fields": {
|
||||||
"advowareAktenzeichen": "Advoware Aktenzeichen",
|
"klaeger": "Kläger",
|
||||||
"aktennr": "Advoware Identifikator",
|
"beklagte": "Beklagte",
|
||||||
"advowareLastSync": "Advoware letzte Synchronisation",
|
"vmhMietverhltnises": "Mietverhältnisse",
|
||||||
"syncStatus": "Sync-Status",
|
"contactsMietinkasso": "Freigegebene Nutzer",
|
||||||
"klaeger": "Kläger",
|
"dokumentesmietinkasso": "Dokumente",
|
||||||
"beklagte": "Beklagte",
|
"gegenstandswert": "Gegenstandswert",
|
||||||
"vmhMietverhltnises": "Mietverhältnisse",
|
"kuendigungsservice": "Kündigungsservice",
|
||||||
"contactsMietinkasso": "Freigegebene Nutzer",
|
"aussergerichtlicheGebuehren13": "Außergerichtliche Gebühren 1,3",
|
||||||
"dokumentesmietinkasso": "Dokumente",
|
"gerichtskosten1Instanz": "Gerichtskosten 1. Instanz",
|
||||||
"gerichtsrubrum": "Gerichtsrubrum",
|
"anwaltskosten1Instanz": "Anwaltskosten 1. Instanz",
|
||||||
"gegenstandswert": "Gegenstandswert",
|
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
||||||
"kuendigungsservice": "Kündigungsservice",
|
"collaborators": "Mitarbeiter",
|
||||||
"aussergerichtlicheGebuehren13": "Außergerichtliche Gebühren 1,3",
|
"vmhVermietersMIK": "Vermieter",
|
||||||
"gerichtskosten1Instanz": "Gerichtskosten 1. Instanz",
|
"advowareAkten": "Advoware Akten",
|
||||||
"anwaltskosten1Instanz": "Anwaltskosten 1. Instanz",
|
"aIKnowledge": "AI Knowledge"
|
||||||
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
},
|
||||||
"collaborators": "Mitarbeiter",
|
"links": {
|
||||||
"vmhVermietersMIK": "Vermieter"
|
"meetings": "Termine",
|
||||||
},
|
"calls": "Anrufe",
|
||||||
"links": {
|
"tasks": "Aufgaben",
|
||||||
"meetings": "Termine",
|
"klaeger": "Kläger",
|
||||||
"calls": "Anrufe",
|
"beklagte": "Beklagte",
|
||||||
"tasks": "Aufgaben",
|
"vmhMietverhltnises": "Mietverhältnisse",
|
||||||
"klaeger": "Kläger",
|
"contactsMietinkasso": "Freigegebene Nutzer",
|
||||||
"beklagte": "Beklagte",
|
"dokumentesmietinkasso": "Dokumente",
|
||||||
"vmhMietverhltnises": "Mietverhältnisse",
|
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
||||||
"contactsMietinkasso": "Freigegebene Nutzer",
|
"collaborators": "Mitarbeiter",
|
||||||
"dokumentesmietinkasso": "Dokumente",
|
"vmhVermietersMIK": "Vermieter",
|
||||||
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
"pulse": "Pulse",
|
||||||
"collaborators": "Mitarbeiter",
|
"advowareAkten": "Advoware Akten",
|
||||||
"vmhVermietersMIK": "Vermieter"
|
"aIKnowledge": "AI Knowledge"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CMietinkasso": "Mietinkasso erstellen"
|
"Create CMietinkasso": "Mietinkasso erstellen"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"advowareAktenzeichen": "Aktenzeichen aus dem Advoware-System",
|
"gegenstandswert": "Wert des Streitgegenstands",
|
||||||
"aktennr": "Eindeutige Inkasso-Nummer aus Advoware",
|
"kuendigungsservice": "Kündigungsservice aktiviert"
|
||||||
"advowareLastSync": "Zeitpunkt der letzten Synchronisation mit Advoware",
|
|
||||||
"syncStatus": "Status der Synchronisation: pending_sync = Warte auf Sync, clean = erfolgreich, unclean = Abweichungen, failed = Fehler"
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"syncStatus": {
|
|
||||||
"pending_sync": "Warte auf Sync",
|
|
||||||
"clean": "Synchronisiert",
|
|
||||||
"unclean": "Abweichungen",
|
|
||||||
"failed": "Fehlgeschlagen"
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
"dokumente": "Dokumente",
|
"dokumente": "Dokumente",
|
||||||
"teamZuordnungen": "Team-Zuordnungen"
|
"teamZuordnungen": "Team-Zuordnungen",
|
||||||
|
"freigeschalteteNutzer": "Freigeschaltete Nutzer"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"syncStatus": "Status der KI-Analyse für neue Dokumente",
|
"syncStatus": "Status der KI-Analyse für neue Dokumente",
|
||||||
|
|||||||
@@ -1,32 +1,27 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"Create CPulsTeamZuordnung": "Team-Zuordnung erstellen",
|
"Create CPulsTeamZuordnung": "Puls-Team-Zuordnung erstellen",
|
||||||
"CPulsTeamZuordnung": "Puls Team-Zuordnung",
|
"CPulsTeamZuordnung": "Puls-Team-Zuordnungen"
|
||||||
"cPulsTeamZuordnung": "Puls Team-Zuordnungen"
|
},
|
||||||
},
|
"fields": {
|
||||||
"fields": {
|
"puls": "Puls",
|
||||||
"name": "Name",
|
"pulsId": "Puls ID",
|
||||||
"puls": "Puls",
|
"team": "Team",
|
||||||
"team": "Team",
|
"teamId": "Team ID",
|
||||||
"aktiv": "Aktiv",
|
"aktiv": "Aktiv",
|
||||||
"abgeschlossen": "Abgeschlossen",
|
"abgeschlossen": "Abgeschlossen",
|
||||||
"abgeschlossenAm": "Abgeschlossen am",
|
"prioritaet": "Priorität"
|
||||||
"abgeschlossenVon": "Abgeschlossen von",
|
},
|
||||||
"prioritaet": "Priorität"
|
"links": {
|
||||||
},
|
"puls": "Puls",
|
||||||
"links": {
|
"team": "Team"
|
||||||
"puls": "Puls",
|
},
|
||||||
"team": "Team",
|
"options": {
|
||||||
"abgeschlossenVon": "Abgeschlossen von"
|
"prioritaet": {
|
||||||
},
|
"Niedrig": "Niedrig",
|
||||||
"tooltips": {
|
"Normal": "Normal",
|
||||||
"aktiv": "Ist diese Team-Zuordnung aktiv? Inaktive werden nicht angezeigt."
|
"Hoch": "Hoch",
|
||||||
},
|
"Dringend": "Dringend"
|
||||||
"options": {
|
}
|
||||||
"prioritaet": {
|
|
||||||
"Niedrig": "Niedrig",
|
|
||||||
"Normal": "Normal",
|
|
||||||
"Hoch": "Hoch"
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,46 @@
|
|||||||
{
|
{
|
||||||
"fields": {
|
"fields": {
|
||||||
"portalUser": "Portalnutzer",
|
"portalUser": "Portalnutzer",
|
||||||
"advowareAktenzeichen": "Advoware Aktenzeichen",
|
"klaeger": "Kläger",
|
||||||
"aktennr": "Advoware Identifikator",
|
"beklagte": "Beklagte",
|
||||||
"advowareLastSync": "Advoware letzte Synchronisation",
|
"vmhMietverhltnises": "Mietverhältnisse",
|
||||||
"syncStatus": "Sync-Status",
|
"contactsRumungsklage": "Freigegebene Nutzer",
|
||||||
"klaeger": "Kläger",
|
"dokumentesvmhraumungsklage": "Dokumente",
|
||||||
"beklagte": "Beklagte",
|
"gerichtsrubrum": "Gerichtsrubrum",
|
||||||
"vmhMietverhltnises": "Mietverhältnisse",
|
"gegenstandswert": "Gegenstandswert",
|
||||||
"contactsRumungsklage": "Freigegebene Nutzer",
|
"kuendigungsservice": "Kündigungsservice",
|
||||||
"dokumentesvmhraumungsklage": "Dokumente",
|
"aussergerichtlicheGebuehren13": "Außergerichtliche Gebühren 1,3",
|
||||||
"gerichtsrubrum": "Gerichtsrubrum",
|
"gerichtskosten1Instanz": "Gerichtskosten 1. Instanz",
|
||||||
"gegenstandswert": "Gegenstandswert",
|
"anwaltskosten1Instanz": "Anwaltskosten 1. Instanz",
|
||||||
"kuendigungsservice": "Kündigungsservice",
|
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
||||||
"aussergerichtlicheGebuehren13": "Außergerichtliche Gebühren 1,3",
|
"collaborators": "Mitarbeiter",
|
||||||
"gerichtskosten1Instanz": "Gerichtskosten 1. Instanz",
|
"vmhVermietersRKL": "Vermieter",
|
||||||
"anwaltskosten1Instanz": "Anwaltskosten 1. Instanz",
|
"advowareAkten": "Advoware Akten",
|
||||||
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
"aIKnowledge": "AI Knowledge"
|
||||||
"collaborators": "Mitarbeiter",
|
},
|
||||||
"vmhVermietersRKL": "Vermieter"
|
"links": {
|
||||||
},
|
"meetings": "Termine",
|
||||||
"links": {
|
"calls": "Anrufe",
|
||||||
"meetings": "Termine",
|
"tasks": "Aufgaben",
|
||||||
"calls": "Anrufe",
|
"klaeger": "Kläger",
|
||||||
"tasks": "Aufgaben",
|
"beklagte": "Beklagte",
|
||||||
"klaeger": "Kläger",
|
"vmhMietverhltnises": "Mietverhältnisse",
|
||||||
"beklagte": "Beklagte",
|
"kuendigungen": "Kündigungen",
|
||||||
"vmhMietverhltnises": "Mietverhältnisse",
|
"contactsRumungsklage": "Freigegebene Nutzer",
|
||||||
"contactsRumungsklage": "Freigegebene Nutzer",
|
"dokumentesvmhraumungsklage": "Dokumente",
|
||||||
"dokumentesvmhraumungsklage": "Dokumente",
|
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
||||||
"freigeschalteteNutzer": "Freigeschaltete Nutzer (veraltet)",
|
"collaborators": "Mitarbeiter",
|
||||||
"collaborators": "Mitarbeiter",
|
"vmhVermietersRKL": "Vermieter",
|
||||||
"vmhVermietersRKL": "Vermieter"
|
"pulse": "Pulse",
|
||||||
},
|
"advowareAkten": "Advoware Akten",
|
||||||
"labels": {
|
"aIKnowledge": "AI Knowledge"
|
||||||
"Create CVmhRumungsklage": "Räumungsklage erstellen"
|
},
|
||||||
},
|
"labels": {
|
||||||
"tooltips": {
|
"Create CVmhRumungsklage": "Räumungsklage erstellen"
|
||||||
"advowareAktenzeichen": "Aktenzeichen aus dem Advoware-System",
|
},
|
||||||
"aktennr": "Eindeutige Klage-Nummer aus Advoware",
|
"tooltips": {
|
||||||
"advowareLastSync": "Zeitpunkt der letzten Synchronisation mit Advoware",
|
"gerichtsrubrum": "Rubrum des Gerichtsverfahrens",
|
||||||
"syncStatus": "Status der Synchronisation: pending_sync = Warte auf Sync, clean = erfolgreich, unclean = Abweichungen, failed = Fehler"
|
"gegenstandswert": "Wert des Streitgegenstands",
|
||||||
},
|
"kuendigungsservice": "Kündigungsservice aktiviert"
|
||||||
"options": {
|
|
||||||
"syncStatus": {
|
|
||||||
"pending_sync": "Warte auf Sync",
|
|
||||||
"clean": "Synchronisiert",
|
|
||||||
"unclean": "Abweichungen",
|
|
||||||
"failed": "Fehlgeschlagen"
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,5 +4,8 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"Log": "Log"
|
"Log": "Log"
|
||||||
|
},
|
||||||
|
"scopeNamesPlural": {
|
||||||
|
"CAdvowareAkten": "Advoware Akten"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"Create CAIKnowledge": "Δημιουργία AI Knowledge"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
custom/Espo/Custom/Resources/i18n/el_GR/CAdvowareAkten.json
Normal file
10
custom/Espo/Custom/Resources/i18n/el_GR/CAdvowareAkten.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"links": {
|
||||||
|
"meetings": "Συναντήσεις",
|
||||||
|
"calls": "Κλήσεις",
|
||||||
|
"tasks": "Εργασίες"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"Create CAdvowareAkten": "Δημιουργία Advoware Akten"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -1 +1,8 @@
|
|||||||
{}
|
{
|
||||||
|
"fields": {
|
||||||
|
"cAICollections": "AI Collections"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"cAICollections": "AI Collections"
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user