diff --git a/ChangeLog.md b/ChangeLog.md index e560ca4..560a67d 100755 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -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) diff --git a/README.md b/README.md index 7023f50..67f0dd4 100755 --- a/README.md +++ b/README.md @@ -2,9 +2,147 @@ Erweitert Rechnungen, Angebote und Kundenaufträge um **Sections**, **Textzeilen** und **Zwischensummen**. +**Version:** 4.2 +**Autor:** Eduard Wisch +**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 diff --git a/ajax/import_from_origin.php b/ajax/import_from_origin.php index 1e93dda..b28a926 100755 --- a/ajax/import_from_origin.php +++ b/ajax/import_from_origin.php @@ -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())); } diff --git a/ajax/sync_to_facturedet.php b/ajax/sync_to_facturedet.php index 2b29a2a..b3af906 100755 --- a/ajax/sync_to_facturedet.php +++ b/ajax/sync_to_facturedet.php @@ -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; } diff --git a/class/actions_subtotaltitle.class.php b/class/actions_subtotaltitle.class.php index b1b0ed0..5746a45 100755 --- a/class/actions_subtotaltitle.class.php +++ b/class/actions_subtotaltitle.class.php @@ -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 diff --git a/core/modules/modSubtotalTitle.class.php b/core/modules/modSubtotalTitle.class.php index fdca161..ebf6f80 100755 --- a/core/modules/modSubtotalTitle.class.php +++ b/core/modules/modSubtotalTitle.class.php @@ -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'; diff --git a/lib/subtotaltitle.lib.php b/lib/subtotaltitle.lib.php index a05b154..ee52bdb 100755 --- a/lib/subtotaltitle.lib.php +++ b/lib/subtotaltitle.lib.php @@ -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; + } +}