Add CBankverbindungen entity with multilingual support, tooltips, and layout definitions

This commit is contained in:
2026-01-23 15:51:42 +01:00
parent 185524e432
commit ac58b51452
14 changed files with 533 additions and 49 deletions

View File

@@ -4,6 +4,10 @@
"approve": true, "approve": true,
"matchCommandLine": true "matchCommandLine": true
}, },
"./custom/scripts/check_and_rebuild.sh": true "./custom/scripts/check_and_rebuild.sh": true,
"/^bash /var/lib/docker/volumes/vmh-espocrm_espocrm/_data/custom/scripts/check_and_rebuild\\.sh$/": {
"approve": true,
"matchCommandLine": true
}
} }
} }

View File

@@ -159,6 +159,88 @@ JSON
layoutAvailabilityList: Array für Feld-Sichtbarkeit in Layouts (z. B. ["list", "detail"]). layoutAvailabilityList: Array für Feld-Sichtbarkeit in Layouts (z. B. ["list", "detail"]).
layoutIgnoreList: Zu ignorierende Layouts. layoutIgnoreList: Zu ignorierende Layouts.
## Internationalisierung (i18n) und Tooltips
**KRITISCH: Mehrsprachige Tooltip-Verwaltung**
EspoCRM verwendet ein hierarchisches Mehrsprachen-System mit **en_US als Basis-Fallback**:
1. **Sprachpriorität**: en_US → aktuelle Sprache (z.B. de_DE)
2. **Problem**: Tooltips in en_US überschreiben Tooltips in anderen Sprachen
3. **Lösung**: Tooltips MÜSSEN in ALLEN Sprachen definiert werden
### Beispiel für korrektes Tooltip-Setup:
**entityDefs/{Entity}.json:**
```json
{
"fields": {
"iban": {
"type": "varchar",
"tooltip": true // Aktiviert Tooltip-Anzeige
}
}
}
```
**i18n/de_DE/{Entity}.json:**
```json
{
"fields": {
"iban": "IBAN"
},
"tooltips": {
"iban": "Internationale Bankkontonummer im Format DE89..."
}
}
```
**i18n/en_US/{Entity}.json:**
```json
{
"fields": {
"iban": "IBAN"
},
"tooltips": {
"iban": "International Bank Account Number in format DE89..."
}
}
```
### Häufige Fehler:
**FALSCH** - Unvollständige en_US-Datei:
```json
{
"fields": [],
"tooltips": {
"iban": "iban2" // Überschreibt deutschen Tooltip!
}
}
```
**RICHTIG** - Vollständige Definitionen in beiden Sprachen:
- Alle Felder in `fields` definieren
- Alle Tooltips in `tooltips` definieren
- Konsistente Struktur über alle Sprachen
### Debugging von Tooltip-Problemen:
1. **Symptom**: Tooltip zeigt nur Feldnamen (z.B. "iban" statt vollständiger Beschreibung)
2. **Ursache**: Fehlerhafte oder fehlende Definition in `i18n/en_US/{Entity}.json`
3. **Prüfung**:
- Existiert `i18n/en_US/{Entity}.json`?
- Enthält es fehlerhafte Tooltip-Definitionen?
- Sind alle Tooltips konsistent über alle Sprachen?
4. **Lösung**: Vervollständige en_US-Datei mit korrekten englischen Übersetzungen
### Best Practices:
- Erstelle **immer** sowohl de_DE als auch en_US Übersetzungen
- Verwende beschreibende Tooltips mit Beispielen und Format-Hinweisen
- Teste Tooltips nach jedem Rebuild in beiden Sprachen
- Bei neuen Feldern: erst i18n-Dateien vollständig ausfüllen, dann Rebuild
3. Auslösen von Änderungen und Rebuild-Prozess 3. Auslösen von Änderungen und Rebuild-Prozess
Was Änderungen auslösen: Was Änderungen auslösen:

View File

@@ -0,0 +1,99 @@
<?php
namespace Espo\Custom\Classes\FormulaFunctions\IbanGroup;
use Espo\Core\Formula\Functions\BaseFunction;
use Espo\Core\Formula\ArgumentList;
/**
* IBAN-Validierung mit Modulo-97-Algorithmus (ISO 13616)
* Mathematische Prüfung ohne externe Datenbank
*/
class ValidateType extends BaseFunction
{
public function process(ArgumentList $args)
{
if (count($args) < 1) {
return false;
}
$iban = $this->evaluate($args[0]);
if (!$iban || !is_string($iban)) {
return false;
}
// Leerzeichen entfernen und in Großbuchstaben umwandeln
$iban = strtoupper(str_replace(' ', '', trim($iban)));
// Basis-Format prüfen: Mindestens 15, maximal 34 Zeichen
$length = strlen($iban);
if ($length < 15 || $length > 34) {
return false;
}
// Format prüfen: 2 Buchstaben (Ländercode) + 2 Ziffern (Prüfziffer) + Rest alphanumerisch
if (!preg_match('/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/', $iban)) {
return false;
}
// Ländercode extrahieren und Länge prüfen (optionale erweiterte Prüfung)
$countryCode = substr($iban, 0, 2);
$expectedLength = $this->getIbanLength($countryCode);
if ($expectedLength && $length !== $expectedLength) {
return false;
}
// Modulo-97-Algorithmus (ISO 13616)
// 1. IBAN umstellen: ersten 4 Zeichen ans Ende
$rearranged = substr($iban, 4) . substr($iban, 0, 4);
// 2. Buchstaben in Zahlen umwandeln (A=10, B=11, ..., Z=35)
$numericString = '';
for ($i = 0; $i < strlen($rearranged); $i++) {
$char = $rearranged[$i];
if (ctype_alpha($char)) {
// A=65 -> 10, B=66 -> 11, ..., Z=90 -> 35
$numericString .= (string)(ord($char) - 55);
} else {
$numericString .= $char;
}
}
// 3. Modulo 97 berechnen (schrittweise wegen großer Zahlen)
$remainder = 0;
for ($i = 0; $i < strlen($numericString); $i++) {
$remainder = ($remainder * 10 + (int)$numericString[$i]) % 97;
}
// 4. Rest muss genau 1 sein für gültige IBAN
return $remainder === 1;
}
/**
* Gibt die erwartete Länge einer IBAN für einen Ländercode zurück
* Quelle: ISO 13616 / SWIFT IBAN Registry
*/
private function getIbanLength(string $countryCode): ?int
{
$lengths = [
'AD' => 24, 'AE' => 23, 'AL' => 28, 'AT' => 20, 'AZ' => 28,
'BA' => 20, 'BE' => 16, 'BG' => 22, 'BH' => 22, 'BR' => 29,
'BY' => 28, 'CH' => 21, 'CR' => 22, 'CY' => 28, 'CZ' => 24,
'DE' => 22, 'DK' => 18, 'DO' => 28, 'EE' => 20, 'EG' => 29,
'ES' => 24, 'FI' => 18, 'FO' => 18, 'FR' => 27, 'GB' => 22,
'GE' => 22, 'GI' => 23, 'GL' => 18, 'GR' => 27, 'GT' => 28,
'HR' => 21, 'HU' => 28, 'IE' => 22, 'IL' => 23, 'IS' => 26,
'IT' => 27, 'JO' => 30, 'KW' => 30, 'KZ' => 20, 'LB' => 28,
'LC' => 32, 'LI' => 21, 'LT' => 20, 'LU' => 20, 'LV' => 21,
'MC' => 27, 'MD' => 24, 'ME' => 22, 'MK' => 19, 'MR' => 27,
'MT' => 31, 'MU' => 30, 'NL' => 18, 'NO' => 15, 'PK' => 24,
'PL' => 28, 'PS' => 29, 'PT' => 25, 'QA' => 29, 'RO' => 24,
'RS' => 22, 'SA' => 24, 'SE' => 24, 'SI' => 19, 'SK' => 24,
'SM' => 27, 'TN' => 24, 'TR' => 26, 'UA' => 29, 'VA' => 22,
'VG' => 24, 'XK' => 20,
];
return $lengths[$countryCode] ?? null;
}
}

View File

@@ -1,5 +1,52 @@
{ {
"labels": { "labels": {
"Create CBankverbindungen": "Bankverbindung erstellen" "Create CBankverbindungen": "Bankverbindung erstellen",
"CBankverbindungen": "Bankverbindungen"
},
"fields": {
"name": "Name",
"iban": "IBAN",
"bic": "BIC/SWIFT",
"kontoinhaber": "Kontoinhaber",
"bankname": "Bankname",
"istAktiv": "Ist aktiv",
"istStandard": "Standard-Konto",
"advowareKontoId": "Advoware Konto-ID",
"advowareLastSync": "Advoware letzte Synchronisation",
"syncStatus": "Sync-Status",
"beteiligte": "Beteiligter",
"createdAt": "Erstellt am",
"modifiedAt": "Geändert am",
"createdBy": "Erstellt von",
"modifiedBy": "Geändert von",
"assignedUser": "Zugewiesen an",
"teams": "Teams"
},
"links": {
"beteiligte": "Beteiligter",
"createdBy": "Erstellt von",
"modifiedBy": "Geändert von",
"assignedUser": "Zugewiesen an",
"teams": "Teams"
},
"tooltips": {
"name": "Bezeichnung der Bankverbindung (z.B. 'Hauptkonto', 'Geschäftskonto', 'Mietkautionskonto')",
"iban": "Internationale Bankkontonummer im Format DE89370400440532013000. Nur Großbuchstaben und Ziffern, 2-stelliger Ländercode gefolgt von Prüfziffer und Kontonummer (max. 34 Zeichen)",
"bic": "Bank Identifier Code (SWIFT-Code) im Format COBADEFFXXX. 8 oder 11 Zeichen: 4 Zeichen Bankcode, 2 Zeichen Ländercode, 2 Zeichen Ortscode, optional 3 Zeichen Filialcode",
"kontoinhaber": "Vollständiger Name des Kontoinhabers, wie auf dem Bankkonto hinterlegt",
"bankname": "Name der kontoführenden Bank (z.B. 'Sparkasse Köln Bonn', 'Deutsche Bank')",
"istAktiv": "Deaktivierte Bankverbindungen werden nicht mehr für neue Transaktionen verwendet, bleiben aber im System erhalten",
"istStandard": "Das Standard-Konto wird automatisch vorgeschlagen, wenn für diesen Beteiligten eine Zahlung erfasst wird. Nur ein Konto kann als Standard markiert sein",
"beteiligte": "Der Beteiligte, dem diese Bankverbindung zugeordnet ist",
"advowareKontoId": "Eindeutige Konto-ID aus Advoware für die Synchronisation. Wird automatisch bei der Synchronisation gesetzt",
"advowareLastSync": "Zeitpunkt der letzten erfolgreichen Synchronisation mit Advoware. Wird automatisch aktualisiert",
"syncStatus": "Status der Advoware-Synchronisation: 'Synchronisiert' = Daten sind aktuell, 'Abweichungen' = Daten weichen ab, 'Fehlgeschlagen' = Sync-Fehler aufgetreten"
},
"options": {
"syncStatus": {
"clean": "Synchronisiert",
"unclean": "Abweichungen",
"failed": "Fehlgeschlagen"
}
} }
} }

View File

@@ -13,6 +13,7 @@
"calls1": "Anrufe", "calls1": "Anrufe",
"contactsBeteiligte": "Freigegebene Nutzer", "contactsBeteiligte": "Freigegebene Nutzer",
"dokumentesBeteiligte": "Dokumente", "dokumentesBeteiligte": "Dokumente",
"bankverbindungens": "Bankverbindungen",
"betnr": "Advoware Identifikator", "betnr": "Advoware Identifikator",
"advowareLastSync": "Advoware letzte Synchronisation", "advowareLastSync": "Advoware letzte Synchronisation",
"syncStatus": "Sync-Status" "syncStatus": "Sync-Status"
@@ -28,7 +29,8 @@
"adressens": "Adressen", "adressens": "Adressen",
"calls1": "Anrufe", "calls1": "Anrufe",
"contactsBeteiligte": "Freigegebene Nutzer", "contactsBeteiligte": "Freigegebene Nutzer",
"dokumentesBeteiligte": "Dokumente" "dokumentesBeteiligte": "Dokumente",
"bankverbindungens": "Bankverbindungen"
}, },
"labels": { "labels": {
"Create CBeteiligte": "Beteiligte erstellen" "Create CBeteiligte": "Beteiligte erstellen"

View File

@@ -1,9 +1,41 @@
{ {
"fields": { "fields": {
"name": "Name",
"iban": "IBAN",
"bic": "BIC/SWIFT",
"kontoinhaber": "Account Holder",
"bankname": "Bank Name",
"istAktiv": "Is Active",
"istStandard": "Default Account",
"advowareKontoId": "Advoware Account ID",
"advowareLastSync": "Advoware Last Sync",
"syncStatus": "Sync Status",
"beteiligte": "Participant"
}, },
"links": { "links": {
"beteiligte": "Participant"
}, },
"labels": { "labels": {
"Create CBankverbindungen": "Create Bankverbindung" "Create CBankverbindungen": "Create Bank Account"
},
"tooltips": {
"name": "Name or description of the bank account (e.g. 'Main Account', 'Business Account', 'Deposit Account')",
"iban": "International Bank Account Number in format DE89370400440532013000. Only uppercase letters and digits, 2-letter country code followed by check digit and account number (max. 34 characters)",
"bic": "Bank Identifier Code (SWIFT code) in format COBADEFFXXX. 8 or 11 characters: 4 characters bank code, 2 characters country code, 2 characters location code, optionally 3 characters branch code",
"kontoinhaber": "Full name of the account holder as registered with the bank",
"bankname": "Name of the bank (e.g. 'Sparkasse Cologne Bonn', 'Deutsche Bank')",
"istAktiv": "Inactive bank accounts are no longer used for new transactions but remain in the system",
"istStandard": "The default account is automatically suggested when a payment is recorded for this participant. Only one account can be marked as default",
"beteiligte": "The participant to whom this bank account belongs",
"advowareKontoId": "Unique account ID from Advoware for synchronization. Automatically set during synchronization",
"advowareLastSync": "Time of last successful synchronization with Advoware. Automatically updated",
"syncStatus": "Advoware synchronization status: 'Synchronized' = data is current, 'Discrepancies' = data differs, 'Failed' = sync error occurred"
},
"options": {
"syncStatus": {
"clean": "Synchronized",
"unclean": "Discrepancies",
"failed": "Failed"
}
} }
} }

View File

@@ -0,0 +1,40 @@
[
{
"label": "Übersicht",
"rows": [
[
{"name": "name"},
{"name": "beteiligte"}
],
[
{"name": "istAktiv"},
{"name": "istStandard"}
]
]
},
{
"label": "Bankdaten",
"rows": [
[
{"name": "iban"},
{"name": "bic"}
],
[
{"name": "kontoinhaber"},
{"name": "bankname"}
]
]
},
{
"label": "Advoware Sync",
"tabBreak": true,
"tabLabel": "Erweitert",
"rows": [
[
{"name": "advowareKontoId"},
{"name": "syncStatus"},
{"name": "advowareLastSync"}
]
]
}
]

View File

@@ -0,0 +1,32 @@
[
{
"rows": [
[
{
"name": "name"
},
{
"name": "istAktiv"
}
],
[
{
"name": "iban"
},
{
"name": "bic"
}
],
[
{
"name": "bankname"
},
{
"name": "istStandard"
}
]
],
"style": "default",
"label": ""
}
]

View File

@@ -0,0 +1,31 @@
[
{
"name": "name",
"link": true,
"width": 20
},
{
"name": "beteiligte",
"width": 20
},
{
"name": "iban",
"width": 20
},
{
"name": "bankname",
"width": 15
},
{
"name": "istAktiv",
"width": 10
},
{
"name": "istStandard",
"width": 10
},
{
"name": "syncStatus",
"width": 10
}
]

View File

@@ -0,0 +1,5 @@
[
{"name": "name"},
{"name": "iban"},
{"name": "istAktiv"}
]

View File

@@ -0,0 +1,12 @@
{
"functionList": [
"__APPEND__",
{
"name": "iban\\validate",
"insertText": "iban\\validate(IBAN)"
}
],
"functionClassNameMap": {
"iban\\validate": "Espo\\Custom\\Classes\\FormulaFunctions\\IbanGroup\\ValidateType"
}
}

View File

@@ -3,10 +3,89 @@
"name": { "name": {
"type": "varchar", "type": "varchar",
"required": true, "required": true,
"pattern": "$noBadCharacters" "pattern": "$noBadCharacters",
"tooltip": true
}, },
"description": { "iban": {
"type": "text" "type": "varchar",
"required": false,
"maxLength": 42,
"pattern": "/^[A-Z]{2}[0-9]{2}[\\s]?[A-Z0-9]{4}[\\s]?[A-Z0-9]{4}[\\s]?[A-Z0-9]{4}[\\s]?[A-Z0-9]{4}[\\s]?[A-Z0-9]{0,4}[\\s]?[A-Z0-9]{0,2}$/i",
"tooltip": true,
"isCustom": true,
"copyToClipboard": true,
"audited": true,
"options": []
},
"bic": {
"type": "varchar",
"required": false,
"maxLength": 11,
"pattern": "/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/i",
"tooltip": true,
"isCustom": true
},
"kontoinhaber": {
"type": "varchar",
"required": false,
"maxLength": 255,
"tooltip": true,
"isCustom": true
},
"bankname": {
"type": "varchar",
"required": false,
"maxLength": 255,
"tooltip": true,
"isCustom": true
},
"istAktiv": {
"type": "bool",
"default": true,
"tooltip": true,
"isCustom": true
},
"istStandard": {
"type": "bool",
"default": false,
"tooltip": true,
"isCustom": true
},
"advowareKontoId": {
"type": "int",
"required": false,
"tooltip": true,
"isCustom": true
},
"advowareLastSync": {
"type": "datetime",
"required": false,
"readOnly": true,
"tooltip": true,
"isCustom": true
},
"syncStatus": {
"type": "enum",
"required": false,
"options": [
"clean",
"unclean",
"failed"
],
"style": {
"clean": "success",
"unclean": "warning",
"failed": "danger"
},
"default": "clean",
"tooltip": true,
"isCustom": true
},
"beteiligte": {
"type": "link",
"required": false,
"tooltip": true,
"isCustom": true
}, },
"createdAt": { "createdAt": {
"type": "datetime", "type": "datetime",
@@ -37,6 +116,12 @@
} }
}, },
"links": { "links": {
"beteiligte": {
"type": "belongsTo",
"entity": "CBeteiligte",
"foreign": "bankverbindungens",
"isCustom": true
},
"createdBy": { "createdBy": {
"type": "belongsTo", "type": "belongsTo",
"entity": "User" "entity": "User"
@@ -60,6 +145,9 @@
"orderBy": "createdAt", "orderBy": "createdAt",
"order": "desc" "order": "desc"
}, },
"formula": {
"beforeSaveApiScript": "// IBAN-Validierung mit Modulo-97-Algorithmus\nif (!string\\isEmpty(iban)) {\n // Leerzeichen entfernen für Validierung\n $ibanClean = string\\replace(iban, ' ', '');\n \n // Mathematische IBAN-Prüfung\n if (!iban\\validate($ibanClean)) {\n recordService\\throwBadRequest('Ungültige IBAN: Die Prüfziffer ist mathematisch falsch. Bitte überprüfen Sie die eingegebene IBAN.');\n }\n}"
},
"indexes": { "indexes": {
"name": { "name": {
"columns": [ "columns": [

View File

@@ -152,6 +152,9 @@
], ],
"tooltip": true, "tooltip": true,
"isCustom": true "isCustom": true
},
"bankverbindungens": {
"type": "linkMultiple"
} }
}, },
"links": { "links": {
@@ -258,6 +261,13 @@
"entity": "CDokumente", "entity": "CDokumente",
"audited": false, "audited": false,
"isCustom": true "isCustom": true
},
"bankverbindungens": {
"type": "hasMany",
"foreign": "beteiligte",
"entity": "CBankverbindungen",
"audited": false,
"isCustom": true
} }
}, },
"collection": { "collection": {

View File

@@ -57,70 +57,70 @@ return [
1 => 'CVmhErstgespraech', 1 => 'CVmhErstgespraech',
2 => 'CMietobjekt', 2 => 'CMietobjekt',
3 => 'CBeteiligte', 3 => 'CBeteiligte',
4 => 'CAdressen', 4 => 'CBankverbindungen',
5 => 'CVmhMietverhltnis', 5 => 'CAdressen',
6 => 'CVmhRumungsklage', 6 => 'CVmhMietverhltnis',
7 => 'CDokumente', 7 => 'CVmhRumungsklage',
8 => (object) [ 8 => 'CDokumente',
9 => (object) [
'type' => 'divider', 'type' => 'divider',
'id' => '342567', 'id' => '342567',
'text' => '$CRM' 'text' => '$CRM'
], ],
9 => 'Contact', 10 => 'Contact',
10 => (object) [ 11 => (object) [
'type' => 'divider', 'type' => 'divider',
'text' => '$Activities', 'text' => '$Activities',
'id' => '219419' 'id' => '219419'
], ],
11 => 'Email', 12 => 'Email',
12 => 'Call', 13 => 'Call',
13 => 'Task', 14 => 'Task',
14 => 'Calendar', 15 => 'Calendar',
15 => (object) [ 16 => (object) [
'type' => 'divider', 'type' => 'divider',
'id' => '655187', 'id' => '655187',
'text' => '$Support' 'text' => '$Support'
], ],
16 => 'Case', 17 => 'Case',
17 => 'KnowledgeBaseArticle', 18 => 'KnowledgeBaseArticle',
18 => (object) [ 19 => (object) [
'type' => 'divider', 'type' => 'divider',
'text' => NULL, 'text' => NULL,
'id' => '137994' 'id' => '137994'
], ],
19 => '_delimiter_', 20 => '_delimiter_',
20 => (object) [ 21 => (object) [
'type' => 'divider', 'type' => 'divider',
'text' => '$Marketing', 'text' => '$Marketing',
'id' => '463280' 'id' => '463280'
], ],
21 => 'Campaign', 22 => 'Campaign',
22 => 'TargetList', 23 => 'TargetList',
23 => (object) [ 24 => (object) [
'type' => 'divider', 'type' => 'divider',
'text' => '$Business', 'text' => '$Business',
'id' => '518202' 'id' => '518202'
], ],
24 => (object) [ 25 => (object) [
'type' => 'divider', 'type' => 'divider',
'text' => '$Organization', 'text' => '$Organization',
'id' => '566592' 'id' => '566592'
], ],
25 => 'User', 26 => 'User',
26 => (object) [ 27 => (object) [
'type' => 'divider', 'type' => 'divider',
'text' => NULL, 'text' => NULL,
'id' => '898671' 'id' => '898671'
], ],
27 => 'Team', 28 => 'Team',
28 => 'WorkingTimeCalendar', 29 => 'WorkingTimeCalendar',
29 => 'EmailTemplate', 30 => 'EmailTemplate',
30 => 'Template', 31 => 'Template',
31 => 'Import', 32 => 'Import',
32 => 'GlobalStream', 33 => 'GlobalStream',
33 => 'Report', 34 => 'Report',
34 => 'CCallQueues', 35 => 'CCallQueues'
35 => 'CBankverbindungen'
], ],
'quickCreateList' => [ 'quickCreateList' => [
0 => 'Account', 0 => 'Account',
@@ -349,8 +349,8 @@ return [
0 => 'youtube.com', 0 => 'youtube.com',
1 => 'google.com' 1 => 'google.com'
], ],
'cacheTimestamp' => 1769177677, 'cacheTimestamp' => 1769179847,
'microtime' => 1769177677.881943, 'microtime' => 1769179847.572829,
'siteUrl' => 'https://crm.bitbylaw.com', 'siteUrl' => 'https://crm.bitbylaw.com',
'fullTextSearchMinLength' => 4, 'fullTextSearchMinLength' => 4,
'appTimestamp' => 1768843902, 'appTimestamp' => 1768843902,