Add IBAN and BIC validation with custom hooks; update localization messages and cache timestamps
This commit is contained in:
181
README.md
181
README.md
@@ -241,6 +241,187 @@ EspoCRM verwendet ein hierarchisches Mehrsprachen-System mit **en_US als Basis-F
|
|||||||
- Teste Tooltips nach jedem Rebuild in beiden Sprachen
|
- Teste Tooltips nach jedem Rebuild in beiden Sprachen
|
||||||
- Bei neuen Feldern: erst i18n-Dateien vollständig ausfüllen, dann Rebuild
|
- Bei neuen Feldern: erst i18n-Dateien vollständig ausfüllen, dann Rebuild
|
||||||
|
|
||||||
|
## Formula-Scripts und Custom PHP-Erweiterungen
|
||||||
|
|
||||||
|
EspoCRM bietet mächtige Erweiterungsmöglichkeiten durch Formula-Scripts und Custom PHP-Funktionen. Diese ermöglichen Validierungen, Berechnungen und Business-Logik direkt beim Speichern von Datensätzen.
|
||||||
|
|
||||||
|
### Formula-Scripts: Grundlagen
|
||||||
|
|
||||||
|
**WICHTIG: Dateistruktur**
|
||||||
|
|
||||||
|
❌ **FALSCH** - Formula in entityDefs:
|
||||||
|
```json
|
||||||
|
// custom/Espo/Custom/Resources/metadata/entityDefs/Entity.json
|
||||||
|
{
|
||||||
|
"fields": {...},
|
||||||
|
"formula": {
|
||||||
|
"beforeSaveApiScript": "..." // FUNKTIONIERT NICHT!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **RICHTIG** - Separate Formula-Datei:
|
||||||
|
```json
|
||||||
|
// custom/Espo/Custom/Resources/metadata/formula/Entity.json
|
||||||
|
{
|
||||||
|
"beforeSaveApiScript": "if (field != null) { ... }"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verfügbare Formula-Script-Typen
|
||||||
|
|
||||||
|
1. **`beforeSaveApiScript`** - Wird vor dem Speichern ausgeführt (UI + API, ab v7.5+)
|
||||||
|
2. **`beforeSaveCustomScript`** - Nur bei internen Saves (ohne API)
|
||||||
|
3. **`afterSaveScript`** - Nach dem Speichern
|
||||||
|
|
||||||
|
**Verwendung**: Validierungen, Berechnungen, Daten-Transformation vor dem Speichern
|
||||||
|
|
||||||
|
### Verfügbare Formula-Funktionen
|
||||||
|
|
||||||
|
**String-Funktionen:**
|
||||||
|
- `string\concatenate(str1, str2)` - Strings verbinden
|
||||||
|
- `string\replace(text, search, replace)` - Ersetzen
|
||||||
|
- `string\substring(text, start, length)` - Teilstring
|
||||||
|
- `string\length(text)` - Länge
|
||||||
|
- `string\test(text, pattern)` - Regex-Test
|
||||||
|
- ⚠️ **NICHT verfügbar**: `string\isEmpty()` → Verwende `field != null && field != ''`
|
||||||
|
|
||||||
|
**Logik:**
|
||||||
|
- `if (condition) { ... }`
|
||||||
|
- `&&`, `||`, `!` (AND, OR, NOT)
|
||||||
|
- `==`, `!=`, `>`, `<`, `>=`, `<=`
|
||||||
|
|
||||||
|
**Fehlerbehandlung:**
|
||||||
|
- `recordService\throwBadRequest('Fehlermeldung')` - Speichern abbrechen mit Fehlermeldung
|
||||||
|
|
||||||
|
### Custom Formula-Funktionen erstellen
|
||||||
|
|
||||||
|
Beispiel: IBAN-Validierung mit Modulo-97-Algorithmus
|
||||||
|
|
||||||
|
**1. PHP-Klasse erstellen:**
|
||||||
|
|
||||||
|
Pfad: `custom/Espo/Custom/Classes/FormulaFunctions/IbanGroup/ValidateType.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
namespace Espo\Custom\Classes\FormulaFunctions\IbanGroup;
|
||||||
|
|
||||||
|
use Espo\Core\Formula\Functions\BaseFunction;
|
||||||
|
use Espo\Core\Formula\ArgumentList;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IBAN-Validierungs-Logik hier
|
||||||
|
// ... (siehe ValidateType.php für vollständige Implementierung)
|
||||||
|
|
||||||
|
return $remainder === 1; // Modulo-97-Check
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Funktion registrieren:**
|
||||||
|
|
||||||
|
Pfad: `custom/Espo/Custom/Resources/metadata/app/formula.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"functionList": [
|
||||||
|
"__APPEND__",
|
||||||
|
{
|
||||||
|
"name": "iban\\validate",
|
||||||
|
"insertText": "iban\\validate(IBAN)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"functionClassNameMap": {
|
||||||
|
"iban\\validate": "Espo\\Custom\\Classes\\FormulaFunctions\\IbanGroup\\ValidateType"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Funktion verwenden:**
|
||||||
|
|
||||||
|
Pfad: `custom/Espo/Custom/Resources/metadata/formula/CBankverbindungen.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"beforeSaveApiScript": "if (iban != null && iban != '') {\n $ibanClean = string\\replace(iban, ' ', '');\n if (!iban\\validate($ibanClean)) {\n recordService\\throwBadRequest('Ungültige IBAN!');\n }\n}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wichtige Hinweise zur Formula-Entwicklung
|
||||||
|
|
||||||
|
**Namespace-Struktur:**
|
||||||
|
- Verzeichnis: `custom/Espo/Custom/Classes/FormulaFunctions/{GroupName}/`
|
||||||
|
- Namespace: `Espo\Custom\Classes\FormulaFunctions\{GroupName}`
|
||||||
|
- Klassenname: `{FunctionName}Type` (z.B. `ValidateType`)
|
||||||
|
- Muss `BaseFunction` erweitern
|
||||||
|
|
||||||
|
**Funktionsnamen:**
|
||||||
|
- Format: `group\functionName` (z.B. `iban\validate`, `string\replace`)
|
||||||
|
- Backslash `\` wird verwendet (nicht `::` oder `/`)
|
||||||
|
|
||||||
|
**Häufige Fehler:**
|
||||||
|
|
||||||
|
| Fehler | Symptom | Lösung |
|
||||||
|
|--------|---------|--------|
|
||||||
|
| Formula in entityDefs statt formula/ | Script wird nicht ausgeführt | Separate `formula/{Entity}.json` erstellen |
|
||||||
|
| `string\isEmpty()` verwendet | Error: "Unknown function" | Verwende `field != null && field != ''` |
|
||||||
|
| Falsche Namespace-Struktur | Funktion nicht gefunden | Prüfe Namespace, Klassenname, Pfad |
|
||||||
|
| Funktion nicht registriert | "Unknown function" | Eintrag in `app/formula.json` erstellen |
|
||||||
|
| Keine `__APPEND__` in functionList | Überschreibt Core-Funktionen | Immer `"__APPEND__"` als erstes Element |
|
||||||
|
|
||||||
|
### Debugging von Formula-Scripts
|
||||||
|
|
||||||
|
**Logs prüfen:**
|
||||||
|
```bash
|
||||||
|
tail -n 100 /var/www/html/data/logs/espo-*.log | grep -i "formula\|error"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Häufige Fehlermeldungen:**
|
||||||
|
- `Unknown function: xxx` → Funktion existiert nicht oder nicht registriert
|
||||||
|
- `Error 500` → Syntax-Fehler im Formula-Script oder PHP-Fehler
|
||||||
|
- `validationFailure` → `throwBadRequest()` wurde aufgerufen
|
||||||
|
|
||||||
|
**Test-Workflow:**
|
||||||
|
1. Formula-Script schreiben
|
||||||
|
2. Rebuild ausführen: `bash custom/scripts/check_and_rebuild.sh`
|
||||||
|
3. Cache leeren (falls nötig): `rm -rf data/cache/*`
|
||||||
|
4. Testdaten speichern und Logs prüfen
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
✅ **Empfohlen:**
|
||||||
|
- Separate Formula-Dateien pro Entity in `metadata/formula/`
|
||||||
|
- Wiederverwendbare Logik in Custom PHP-Funktionen auslagern
|
||||||
|
- Aussagekräftige Fehlermeldungen mit `throwBadRequest()`
|
||||||
|
- Null-Checks vor Operationen: `field != null && field != ''`
|
||||||
|
- Code kommentieren für spätere Wartung
|
||||||
|
|
||||||
|
❌ **Vermeiden:**
|
||||||
|
- Formula-Scripts in `entityDefs` ablegen
|
||||||
|
- Nicht-existente String-Funktionen wie `isEmpty()`
|
||||||
|
- Komplexe Logik direkt in Formula (besser: PHP-Funktion)
|
||||||
|
- Fehlende Registrierung in `app/formula.json`
|
||||||
|
|
||||||
|
### Beispiel-Anwendungsfälle
|
||||||
|
|
||||||
|
1. **Validierung**: IBAN-Check, Email-Format, Telefonnummern
|
||||||
|
2. **Berechnung**: Gesamtpreise, Datumsberechnungen, Provisionen
|
||||||
|
3. **Daten-Transformation**: Großbuchstaben, Formatierungen, Normalisierungen
|
||||||
|
4. **Business Rules**: Status-Überprüfungen, Pflichtfeld-Logik, Abhängigkeiten
|
||||||
|
|
||||||
3. Auslösen von Änderungen und Rebuild-Prozess
|
3. Auslösen von Änderungen und Rebuild-Prozess
|
||||||
|
|
||||||
Was Änderungen auslösen:
|
Was Änderungen auslösen:
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Espo\Custom\Hooks\CBankverbindungen;
|
||||||
|
|
||||||
|
use Espo\ORM\Entity;
|
||||||
|
use Espo\Core\Exceptions\BadRequest;
|
||||||
|
use Espo\Core\Utils\Language;
|
||||||
|
|
||||||
|
class BankdatenValidation
|
||||||
|
{
|
||||||
|
private $language;
|
||||||
|
|
||||||
|
public function __construct(Language $language)
|
||||||
|
{
|
||||||
|
$this->language = $language;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function beforeSave(Entity $entity, array $options): void
|
||||||
|
{
|
||||||
|
// IBAN-Normalisierung und Validierung
|
||||||
|
$iban = $entity->get('iban');
|
||||||
|
if ($iban !== null && $iban !== '') {
|
||||||
|
// IBAN normalisieren: Leerzeichen entfernen und Großbuchstaben
|
||||||
|
$ibanClean = strtoupper(str_replace(' ', '', $iban));
|
||||||
|
$entity->set('iban', $ibanClean);
|
||||||
|
|
||||||
|
// Mathematische IBAN-Prüfung mit Modulo-97
|
||||||
|
if (!$this->validateIban($ibanClean)) {
|
||||||
|
$message = $this->language->translateLabel('invalidIbanChecksum', 'messages', 'CBankverbindungen');
|
||||||
|
throw new BadRequest($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BIC-Normalisierung und Validierung
|
||||||
|
$bic = $entity->get('bic');
|
||||||
|
if ($bic !== null && $bic !== '') {
|
||||||
|
// BIC normalisieren: Leerzeichen entfernen und Großbuchstaben
|
||||||
|
$bicClean = strtoupper(str_replace(' ', '', $bic));
|
||||||
|
$entity->set('bic', $bicClean);
|
||||||
|
|
||||||
|
// BIC-Format prüfen: 8 oder 11 Zeichen
|
||||||
|
$bicLength = strlen($bicClean);
|
||||||
|
if ($bicLength !== 8 && $bicLength !== 11) {
|
||||||
|
$message = $this->language->translateLabel('invalidBicLength', 'messages', 'CBankverbindungen');
|
||||||
|
throw new BadRequest($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BIC-Format prüfen: Regex
|
||||||
|
if (!preg_match('/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/', $bicClean)) {
|
||||||
|
$message = $this->language->translateLabel('invalidBicFormat', 'messages', 'CBankverbindungen');
|
||||||
|
throw new BadRequest($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateIban(string $iban): bool
|
||||||
|
{
|
||||||
|
// IBAN-Länge prüfen (mindestens 15 Zeichen)
|
||||||
|
if (strlen($iban) < 15) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IBAN umstellen: erste 4 Zeichen ans Ende
|
||||||
|
$rearranged = substr($iban, 4) . substr($iban, 0, 4);
|
||||||
|
|
||||||
|
// Buchstaben in Zahlen umwandeln (A=10, B=11, ..., Z=35)
|
||||||
|
$numeric = '';
|
||||||
|
for ($i = 0; $i < strlen($rearranged); $i++) {
|
||||||
|
$char = $rearranged[$i];
|
||||||
|
if (ctype_alpha($char)) {
|
||||||
|
$numeric .= (string)(ord($char) - ord('A') + 10);
|
||||||
|
} else {
|
||||||
|
$numeric .= $char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modulo-97-Prüfung
|
||||||
|
return $this->bcmod($numeric, '97') === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bcmod(string $number, string $modulus): string
|
||||||
|
{
|
||||||
|
// Für große Zahlen: Modulo in Schritten berechnen
|
||||||
|
$take = 9;
|
||||||
|
$mod = '';
|
||||||
|
|
||||||
|
do {
|
||||||
|
$a = (int)($mod . substr($number, 0, $take));
|
||||||
|
$number = substr($number, $take);
|
||||||
|
$mod = (string)($a % (int)$modulus);
|
||||||
|
} while (strlen($number) > 0);
|
||||||
|
|
||||||
|
return $mod;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,5 +48,10 @@
|
|||||||
"unclean": "Abweichungen",
|
"unclean": "Abweichungen",
|
||||||
"failed": "Fehlgeschlagen"
|
"failed": "Fehlgeschlagen"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"invalidIbanChecksum": "Die eingegebene IBAN ist ungueltig. Bitte pruefen Sie die Nummer auf Tippfehler.",
|
||||||
|
"invalidBicLength": "Der BIC/SWIFT-Code muss 8 oder 11 Zeichen lang sein.",
|
||||||
|
"invalidBicFormat": "Der eingegebene BIC/SWIFT-Code hat ein ungueltiges Format. Beispiel: COBADEFFXXX"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,5 +37,10 @@
|
|||||||
"unclean": "Discrepancies",
|
"unclean": "Discrepancies",
|
||||||
"failed": "Failed"
|
"failed": "Failed"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"invalidIbanChecksum": "The entered IBAN is invalid. Please check for typos.",
|
||||||
|
"invalidBicLength": "The BIC/SWIFT code must be 8 or 11 characters long.",
|
||||||
|
"invalidBicFormat": "The entered BIC/SWIFT code has an invalid format. Example: COBADEFFXXX"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
"type": "varchar",
|
"type": "varchar",
|
||||||
"required": false,
|
"required": false,
|
||||||
"maxLength": 11,
|
"maxLength": 11,
|
||||||
"pattern": "/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/i",
|
|
||||||
"tooltip": true,
|
"tooltip": true,
|
||||||
"isCustom": true
|
"isCustom": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"beforeSaveApiScript": "// IBAN-Validierung mit Modulo-97-Algorithmus\nif (iban != null && 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}"
|
"beforeSaveApiScript": ""
|
||||||
}
|
}
|
||||||
@@ -349,8 +349,8 @@ return [
|
|||||||
0 => 'youtube.com',
|
0 => 'youtube.com',
|
||||||
1 => 'google.com'
|
1 => 'google.com'
|
||||||
],
|
],
|
||||||
'cacheTimestamp' => 1769180353,
|
'cacheTimestamp' => 1769181610,
|
||||||
'microtime' => 1769180353.404299,
|
'microtime' => 1769181610.43814,
|
||||||
'siteUrl' => 'https://crm.bitbylaw.com',
|
'siteUrl' => 'https://crm.bitbylaw.com',
|
||||||
'fullTextSearchMinLength' => 4,
|
'fullTextSearchMinLength' => 4,
|
||||||
'appTimestamp' => 1768843902,
|
'appTimestamp' => 1768843902,
|
||||||
|
|||||||
Reference in New Issue
Block a user