Version 4.2: PostgreSQL-Kompatibilität, GlobalNotify, Dokumentation

- fix: DELETE mit JOIN durch Subquery ersetzt (PostgreSQL-kompatibel)
- feat: GlobalNotify Integration für Fehler-Benachrichtigungen
- feat: subtotaltitle_notify() Helper-Funktion
- docs: ChangeLog.md komplett überarbeitet (alle Versionen)
- docs: README.md aktualisiert mit allen Features und Dateistruktur

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-03-03 15:01:51 +01:00
parent 9a8f5431e1
commit 27481bab31
7 changed files with 297 additions and 127 deletions

View file

@ -1,10 +1,73 @@
# CHANGELOG MODULE SUBTOTALTITLE FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
## 1.1
Alle wichtigen Änderungen werden hier dokumentiert.
- **Zwischensumme bei Section-Löschung mitlöschen**: Wenn eine Produktgruppe über Dolibarrs Standard-Löschbutton (confirm_deleteline) gelöscht wird, werden zugehörige Zwischensummen automatisch aus facturedet und facture_lines_manager mitgelöscht
- **Verwaiste Subtotals aufräumen**: Nach jeder Zeilenlöschung werden verwaiste Zwischensummen (parent_section zeigt auf nicht-existierende Section) automatisch bereinigt
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/),
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
## 1.0
## [4.2] - 2026-03-03
Initial version
### Fixed
- **Datenbank-Kompatibilität**: DELETE mit JOIN durch Subquery ersetzt für PostgreSQL-Kompatibilität
- **Duplikat-Schutz für Subtotals**: Prüfung in Detail-Tabelle UND Manager-Tabelle vor dem Erstellen
- **SQL-Injection-Schutz**: `addslashes()` durch `$db->escape()` ersetzt
- **Präzisere Duplikat-Erkennung**: LIKE durch exakten Match ersetzt
## [4.1] - 2026-03-02
### Added
- **Import-Funktion für Produktgruppen**: Sections, Textzeilen und Subtotals können von Angebot zu Auftrag und von Auftrag zu Rechnung importiert werden
- **Automatisches Produkt-Mapping**: Produkte werden anhand der `fk_product` den passenden Sections zugeordnet
- **Import-Button in GUI**: Neuer Button "Import von [Ursprungsdokument]" mit Bestätigungsdialog
### Changed
- **Rang-Synchronisation**: `rang` in Dolibarr-Tabelle und `line_order` in Manager-Tabelle werden beim Import synchronisiert
## [4.0] - 2026-02-28
### Added
- **Multi-Dokument-Support**: Vollständige Unterstützung für Rechnungen, Angebote UND Kundenaufträge
- **DocumentTypeHelper-Klasse**: Zentrale Verwaltung der Tabellennamen pro Dokumenttyp
- **Einheitliche API**: Alle AJAX-Endpunkte unterstützen `document_type` Parameter
### Changed
- **Datenbank-Schema erweitert**: `fk_propal`, `fk_commande`, `fk_propaldet`, `fk_commandedet` hinzugefügt
- **Hooks erweitert**: `propalcard` und `ordercard` zum Hook-Array hinzugefügt
## [3.0] - 2026-02-25
### Added
- **Dokument-Synchronisation**: "Zum Dokument" / "Aus Dokument" Buttons für Batch-Sync
- **In-Dokument-Checkbox**: Einzelne Elemente können zum PDF hinzugefügt/entfernt werden
- **Grüner Rand**: Visuelle Kennzeichnung für im Dokument enthaltene Elemente
- **ODT-Substitutionsvariablen**: `line_is_section`, `line_is_textline`, `line_is_subtotal`, etc.
### Changed
- **special_code Werte**: 100=Section, 101=Text, 102=Subtotal
## [2.0] - 2026-02-20
### Added
- **Drag & Drop Sortierung**: Sections und Produkte können per Drag & Drop sortiert werden
- **Link-Button**: Produkte können über Button einer Section zugeordnet werden
- **Collapse/Expand**: Sections können ein-/ausgeklappt werden
- **Verwaiste Zeilen Button**: Button zum Entfernen verwaister Manager-Einträge
### Fixed
- **Hardcodierte Pfade**: Relative Pfade statt absoluter Pfade
## [1.1] - 2026-02-15
### Fixed
- **Zwischensumme bei Section-Löschung**: Zugehörige Subtotals werden beim Löschen einer Section mitgelöscht
- **Verwaiste Subtotals aufräumen**: Automatische Bereinigung nach jeder Zeilenlöschung
## [1.0] - 2026-02-01
### Added
- Initiale Version
- **Sections (Überschriften)**: Produkte in Gruppen organisieren
- **Textzeilen**: Freie Texte ohne Preis einfügen
- **Zwischensummen**: Automatische Berechnung pro Section
- **Manager-Tabelle**: `llx_facture_lines_manager` für Strukturverwaltung
- Nur Rechnungen-Support (facturedet)

264
README.md
View file

@ -2,9 +2,147 @@
Erweitert Rechnungen, Angebote und Kundenaufträge um **Sections**, **Textzeilen** und **Zwischensummen**.
**Version:** 4.2
**Autor:** Eduard Wisch <data@data-it-solution.de>
**Lizenz:** GPL v3+
---
## 🔑 ODT Template Schlüsselwörter
## Features
- **Sections (Produktgruppen)**: Produkte in logische Gruppen organisieren
- **Textzeilen**: Freie Texte ohne Preis einfügen
- **Zwischensummen**: Automatische Berechnung pro Section
- **Drag & Drop**: Sortierung per Drag & Drop
- **Dokument-Synchronisation**: Elemente zum PDF hinzufügen/entfernen
- **Import-Funktion**: Sections von Angebot zu Auftrag zu Rechnung übernehmen
- **ODT-Variablen**: Bedingte Formatierung in ODT-Templates
---
## Unterstützte Dokumenttypen
| Dokumenttyp | Tabelle | Status |
|-------------|---------|--------|
| Rechnungen (Factures) | llx_facturedet | Vollständig |
| Angebote (Propals) | llx_propaldet | Vollständig |
| Kundenaufträge (Commandes) | llx_commandedet | Vollständig |
---
## Modul-Funktionen
### Sections (Überschriften)
- Erstellen von Überschriften zur Strukturierung
- Produkte können per Drag & Drop oder Link-Button zugeordnet werden
- Optional: Zwischensumme für jede Section anzeigen
- Ein-/Ausklappen von Sections
### Textzeilen
- Freie Textzeilen ohne Preis
- Ideal für Hinweise, Bedingungen oder Erklärungen
### Zwischensummen
- Automatische Berechnung der Summe aller Produkte in einer Section
- Checkbox zum Ein-/Ausschalten pro Section
- Duplikat-Schutz verhindert doppelte Subtotals
### Dokument-Synchronisation
- **Checkbox** bei jeder Section/Textzeile/Subtotal: Element zum Dokument hinzufügen
- **Zum Dokument / Aus Dokument** Buttons: Alle Elemente auf einmal synchronisieren
- **Grüner Rand** = Element ist im Dokument/PDF enthalten
### Import-Funktion (NEU in v4.1)
- Sections und Textzeilen werden von Ursprungsdokument importiert
- Produkte werden automatisch den passenden Sections zugeordnet (via fk_product)
- Unterstützt: Angebot → Auftrag → Rechnung
- Button "Import von [Ursprungsdokument]" mit Bestätigungsdialog
---
## Installation
### 1. Dateien kopieren
```
htdocs/custom/subtotaltitle/
├── class/
│ ├── actions_subtotaltitle.class.php
│ └── DocumentTypeHelper.class.php
├── ajax/
│ ├── add_to_section.php
│ ├── assign_last_product.php
│ ├── check_subtotal.php
│ ├── cleanup_subtotals.php
│ ├── create_section.php
│ ├── create_textline.php
│ ├── delete_section.php
│ ├── delete_textline.php
│ ├── edit_textline.php
│ ├── fix_section_hierarchy.php
│ ├── fix_sections.php
│ ├── get_line_orders.php
│ ├── get_sections.php
│ ├── get_textlines.php
│ ├── import_from_origin.php # NEU in v4.1
│ ├── mass_delete.php
│ ├── move_product.php
│ ├── move_section.php
│ ├── remove_from_section.php
│ ├── rename_section.php
│ ├── reorder_all.php
│ ├── reorder_invoice.php
│ ├── repair_missing_subtotals.php
│ ├── sync_to_facturedet.php
│ └── toggle_subtotal.php
├── core/
│ ├── modules/
│ │ └── modSubtotalTitle.class.php
│ └── substitutions/
│ └── functions_subtotaltitle.lib.php
├── js/
│ └── subtotaltitle.js
├── css/
│ └── subtotaltitle.css
├── lib/
│ └── subtotaltitle.lib.php
└── sql/
└── llx_facture_lines_manager.sql
```
### 2. Modul aktivieren
Im Dolibarr Backend unter **Home → Setup → Modules** das Modul **SubtotalTitle** aktivieren.
Die Datenbanktabelle `llx_facture_lines_manager` wird automatisch erstellt.
---
## Datenbank
### Tabelle: llx_facture_lines_manager
| Feld | Typ | Beschreibung |
|------|-----|--------------|
| rowid | INT | Primary Key |
| fk_facture | INT | FK zu Rechnung |
| fk_propal | INT | FK zu Angebot |
| fk_commande | INT | FK zu Kundenauftrag |
| document_type | VARCHAR(20) | 'invoice', 'propal', 'order' |
| line_type | VARCHAR(20) | 'section', 'product', 'text', 'subtotal' |
| fk_facturedet | INT | FK zu llx_facturedet |
| fk_propaldet | INT | FK zu llx_propaldet |
| fk_commandedet | INT | FK zu llx_commandedet |
| title | VARCHAR(255) | Titel für Sections/Text |
| parent_section | INT | FK zur übergeordneten Section |
| line_order | INT | Sortierreihenfolge |
| show_subtotal | TINYINT | Zwischensumme anzeigen (0/1) |
| collapsed | TINYINT | Section eingeklappt (0/1) |
| in_facturedet | TINYINT | Im Dokument enthalten (0/1) |
---
## ODT Template Schlüsselwörter
### Zeilen-Variablen (pro Zeile in row.lines)
@ -77,128 +215,26 @@ Zwischensumme: {line_price_ht_locale} €
---
## 📋 Modul-Funktionen
## Anforderungen
### Sections (Überschriften)
- Erstellen von Überschriften zur Strukturierung
- Produkte können per Drag & Drop oder Link-Button zugeordnet werden
- Optional: Zwischensumme für jede Section anzeigen
- Ein-/Ausklappen von Sections
### Textzeilen
- Freie Textzeilen ohne Preis
- Ideal für Hinweise, Bedingungen oder Erklärungen
### Zwischensummen
- Automatische Berechnung der Summe aller Produkte in einer Section
- Checkbox zum Ein-/Ausschalten pro Section
### Dokument-Synchronisation
- **📄 Checkbox** bei jeder Section/Textzeile/Subtotal: Element zum Dokument hinzufügen
- **→ Zum Dokument / ← Aus Dokument** Buttons: Alle Elemente auf einmal synchronisieren
- **Grüner Rand** = Element ist im Dokument/PDF enthalten
- Buttons werden nur angezeigt wenn Sections oder Textzeilen vorhanden sind
### Unterstützte Dokumenttypen
- Rechnungen (Factures)
- Angebote (Propals)
- Kundenaufträge (Commandes)
- Dolibarr >= 16.0
- PHP >= 7.4
- MySQL >= 5.7 / MariaDB >= 10.3 / PostgreSQL >= 12
---
## 🔧 Installation
## Changelog
### 1. Dateien kopieren
Siehe [ChangeLog.md](ChangeLog.md) für die vollständige Versionshistorie.
```
htdocs/custom/subtotaltitle/
├── class/
│ ├── actions_subtotaltitle.class.php
│ └── DocumentTypeHelper.class.php
├── ajax/
│ ├── add_to_section.php
│ ├── assign_last_product.php
│ ├── cleanup_subtotals.php
│ ├── create_section.php
│ ├── create_textline.php
│ ├── get_textlines.php
│ ├── move_section.php
│ ├── reorder_all.php
│ ├── sync_to_facturedet.php
│ └── toggle_subtotal.php
├── core/
│ ├── modules/
│ │ └── modSubtotalTitle.class.php
│ └── substitutions/
│ └── functions_subtotaltitle.lib.php
├── js/
│ └── subtotaltitle.js
├── css/
│ └── subtotaltitle.css
├── lib/
│ └── subtotaltitle.lib.php
└── sql/
└── llx_facture_lines_manager.sql
```
### 2. Modul aktivieren
Im Dolibarr Backend unter **Home → Setup → Modules** das Modul **SubtotalTitle** aktivieren.
Die Datenbanktabelle `llx_facture_lines_manager` wird automatisch erstellt.
### 3. Modul-Konfiguration
In `core/modules/modSubtotalTitle.class.php` muss folgendes gesetzt sein:
```php
$this->module_parts = array(
'substitutions' => 1, // Für ODT-Variablen
'hooks' => array(
'data' => array('invoicecard', 'propalcard', 'ordercard'),
'entity' => '0'
)
);
```
### Aktuelle Version (4.2)
- Datenbank-Kompatibilität für PostgreSQL verbessert
- Duplikat-Schutz für Subtotals
- SQL-Injection-Schutz verbessert
---
## 🗄️ Datenbank
### Tabelle: llx_facture_lines_manager
| Feld | Typ | Beschreibung |
|------|-----|--------------|
| rowid | INT | Primary Key |
| fk_facture | INT | FK zu Rechnung |
| fk_propal | INT | FK zu Angebot |
| fk_commande | INT | FK zu Kundenauftrag |
| document_type | VARCHAR(20) | 'invoice', 'propal', 'order' |
| line_type | VARCHAR(20) | 'section', 'product', 'text', 'subtotal' |
| fk_facturedet | INT | FK zu llx_facturedet |
| fk_propaldet | INT | FK zu llx_propaldet |
| fk_commandedet | INT | FK zu llx_commandedet |
| title | VARCHAR(255) | Titel für Sections/Text |
| parent_section | INT | FK zur übergeordneten Section |
| line_order | INT | Sortierreihenfolge |
| show_subtotal | TINYINT | Zwischensumme anzeigen (0/1) |
| collapsed | TINYINT | Section eingeklappt (0/1) |
| in_facturedet | TINYINT | Im Dokument enthalten (0/1) |
---
## 📝 Changelog
### Version 1.0
- Initiale Version mit Section-, Text- und Subtotal-Unterstützung
- ODT-Substitutionsvariablen
- Multi-Dokument-Support (Rechnungen, Angebote, Kundenaufträge)
- Drag & Drop Sortierung
- Dokument-Synchronisation
---
## 📄 Lizenz
## Lizenz
Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>

View file

@ -557,6 +557,14 @@ if ($action == 'check') {
} catch (Exception $e) {
$db->rollback();
subtotaltitle_debug_log('❌ Import fehlgeschlagen: '.$e->getMessage());
// Benachrichtigung über GlobalNotify (falls verfügbar)
subtotaltitle_notify(
'error',
'Import fehlgeschlagen',
'Produktgruppen-Import von '.$origin['type'].' #'.$origin['id'].' nach '.$target_type.' #'.$target_id.' fehlgeschlagen: '.$e->getMessage()
);
echo json_encode(array('success' => false, 'error' => $e->getMessage()));
}

View file

@ -211,7 +211,13 @@ if ($action == 'add') {
subtotaltitle_debug_log('📝 SQL: '.$sql_ins);
if (!$db->query($sql_ins)) {
echo json_encode(array('success' => false, 'error' => $db->lasterror()));
$error_msg = $db->lasterror();
subtotaltitle_notify(
'error',
'Sync fehlgeschlagen',
'Fehler beim Hinzufügen zu '.$tables['lines_table'].': '.$error_msg
);
echo json_encode(array('success' => false, 'error' => $error_msg));
exit;
}

View file

@ -1271,12 +1271,14 @@ class ActionsSubtotalTitle extends CommonHookActions
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) return;
// 1. CLEANUP: Lösche verwaiste Einträge
$sql_cleanup = "DELETE m FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql_cleanup .= " LEFT JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON m.".$tables['fk_line']." = d.rowid";
$sql_cleanup .= " WHERE m.".$tables['fk_parent']." = ".(int)$document_id;
$sql_cleanup .= " AND m.line_type = 'product'";
$sql_cleanup .= " AND d.rowid IS NULL";
// 1. CLEANUP: Lösche verwaiste Einträge (Produkte deren Detail-Zeile nicht mehr existiert)
// Subquery-basiert für Kompatibilität mit MySQL, PostgreSQL und anderen Datenbanken
$sql_cleanup = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_cleanup .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_cleanup .= " AND document_type = '".$db->escape($docType)."'";
$sql_cleanup .= " AND line_type = 'product'";
$sql_cleanup .= " AND ".$tables['fk_line']." IS NOT NULL";
$sql_cleanup .= " AND ".$tables['fk_line']." NOT IN (SELECT rowid FROM ".MAIN_DB_PREFIX.$tables['lines_table'].")";
$result = $db->query($sql_cleanup);
// 2. Hole alle Produktzeilen des Dokuments mit rang

View file

@ -76,7 +76,7 @@ class modSubtotalTitle extends DolibarrModules
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@subtotaltitle'
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
$this->version = '4.1';
$this->version = '4.4';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';

View file

@ -103,3 +103,58 @@ function subtotaltitle_debug_log($message, $force = false)
error_log('[SubtotalTitle] ' . $message);
}
}
/**
* Sendet Benachrichtigung über GlobalNotify (falls verfügbar)
* Fallback: Schreibt ins Dolibarr-Log
*
* @param string $type 'error', 'warning', 'info', 'action'
* @param string $title Kurzer Titel
* @param string $message Detaillierte Nachricht
* @param string $actionUrl URL für Aktions-Button (optional)
* @param string $actionLabel Label für Aktions-Button (optional)
* @return bool True wenn über GlobalNotify gesendet
*/
function subtotaltitle_notify($type, $title, $message, $actionUrl = '', $actionLabel = '')
{
global $db;
// Prüfe ob GlobalNotify aktiv und verfügbar
if (!isModEnabled('globalnotify')) {
dol_syslog("SubtotalTitle [{$type}]: {$title} - {$message}", LOG_WARNING);
return false;
}
$classFile = dol_buildpath('/globalnotify/class/globalnotify.class.php', 0);
if (!file_exists($classFile)) {
dol_syslog("SubtotalTitle [{$type}]: {$title} - {$message}", LOG_WARNING);
return false;
}
require_once $classFile;
if (!class_exists('GlobalNotify')) {
dol_syslog("SubtotalTitle [{$type}]: {$title} - {$message}", LOG_WARNING);
return false;
}
try {
switch ($type) {
case 'error':
GlobalNotify::error('subtotaltitle', $title, $message, $actionUrl, $actionLabel);
break;
case 'warning':
GlobalNotify::warning('subtotaltitle', $title, $message, $actionUrl, $actionLabel);
break;
case 'action':
GlobalNotify::actionRequired('subtotaltitle', $title, $message, $actionUrl, $actionLabel ?: 'Aktion erforderlich');
break;
default:
GlobalNotify::info('subtotaltitle', $title, $message, $actionUrl, $actionLabel);
}
return true;
} catch (Exception $e) {
dol_syslog("GlobalNotify error: ".$e->getMessage(), LOG_ERR);
return false;
}
}