Compare commits

...

No commits in common. "v3.5-facture" and "main" have entirely different histories.

40 changed files with 4942 additions and 1116 deletions

7
.claude/settings.json Executable file
View file

@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(grep:*)"
]
}
}

View file

@ -1,2 +0,0 @@
[Buildset]
BuildItems=@Variant(\x00\x00\x00\t\x00\x00\x00\x00\x01\x00\x00\x00\x0b\x00\x00\x00\x00\x01\x00\x00\x00\x1a\x00s\x00u\x00b\x00t\x00o\x00t\x00a\x00l\x00t\x00i\x00t\x00l\x00e)

166
MIGRATION_MULTITYPE.md Executable file
View file

@ -0,0 +1,166 @@
# Migration zu Multi-Dokumenttyp-Unterstützung
## Übersicht
Diese Migration erweitert das SubtotalTitle-Modul von der ausschließlichen Unterstützung für Rechnungen auf:
- ✅ **Rechnungen** (invoice/facture)
- ✅ **Angebote** (propal)
- ✅ **Kundenaufträge** (order/commande)
## Durchgeführte Änderungen
### 1. Datenbank-Schema (✅ Erledigt)
**Datei:** `sql/llx_facture_lines_manager.sql`
Neue Spalten zur Tabelle `llx_facture_lines_manager`:
- `document_type` VARCHAR(20) - Art des Dokuments ('invoice', 'propal', 'order')
- `fk_propal` INT(11) - Referenz auf Angebot
- `fk_commande` INT(11) - Referenz auf Kundenauftrag
- `fk_propaldet` INT(11) - Referenz auf Angebots-Zeile
- `fk_commandedet` INT(11) - Referenz auf Auftrags-Zeile
**Indizes** für bessere Performance hinzugefügt.
### 2. Modul-Konfiguration (✅ Erledigt)
**Datei:** `core/modules/modSubtotalTitle.class.php`
- Hooks erweitert: `invoicecard`, `propalcard`, `ordercard`
- Beschreibung aktualisiert
### 3. Helper-Klasse (✅ Neu erstellt)
**Datei:** `class/DocumentTypeHelper.class.php`
Zentrale Klasse zur Verwaltung verschiedener Dokumenttypen:
- `getTypeFromContext()` - Erkennt Typ aus Hook-Context
- `getTypeFromObject()` - Erkennt Typ aus Dolibarr-Objekt
- `getTableNames()` - Liefert DB-Tabellennamen für jeden Typ
- `getContext()` - Liefert Hook-Context für jeden Typ
### 4. Hook-Implementierung (✅ Teilweise erledigt)
**Datei:** `class/actions_subtotaltitle.class.php`
Angepasste Methoden:
- `formObjectOptions()` - Unterstützt alle 3 Dokumenttypen
- `printObjectLine()` - Generisch für alle Typen
- `formAddObjectLine()` - Generisch für alle Typen
- `syncManagerTable()` - Generisch für alle Typen
- `renderSectionDropdown()` - Generisch für alle Typen
- `getNextLineOrder()` - Generisch für alle Typen
**WICHTIG:** Die Methode `renderAllPendingSections()` wurde teilweise angepasst, benötigt aber noch weitere Überprüfung.
### 5. JavaScript (⚠️ Teilweise erledigt)
**Datei:** `js/subtotaltitle.js`
Hinzugefügt:
- `getDocumentType()` - Erkennt Dokumenttyp aus URL
**TODO:** JavaScript-Code muss noch vollständig generisch gemacht werden.
### 6. AJAX-Dateien (❌ Noch zu erledigen)
Alle AJAX-Dateien im Verzeichnis `ajax/` müssen angepasst werden:
- `create_section.php`
- `move_section.php`
- `delete_section.php`
- `rename_section.php`
- `create_textline.php`
- `edit_textline.php`
- `delete_textline.php`
- `assign_last_product.php`
- `move_product.php`
- `remove_from_section.php`
- `reorder_all.php`
- `toggle_subtotal.php`
- `mass_delete.php`
- `sync_to_facturedet.php`
- `get_sections.php`
- `get_textlines.php`
- `get_line_orders.php`
**Anpassung:** Jede Datei muss:
1. `document_type` Parameter empfangen/erkennen
2. `DocumentTypeHelper` verwenden
3. Korrekte FK-Spalten verwenden (fk_facture/fk_propal/fk_commande)
## Installationsschritte
### Schritt 1: Datenbank Migration
```bash
mysql -u root -p dolibarr < /srv/http/dolibarr/custom/subtotaltitle/sql/llx_facture_lines_manager.sql
```
### Schritt 2: Modul neu laden
1. In Dolibarr: Home → Setup → Modules
2. SubtotalTitle Modul deaktivieren
3. SubtotalTitle Modul aktivieren
### Schritt 3: Cache leeren
```bash
rm -rf /srv/http/dolibarr/documents/admin/temp/*
```
## Testen
### Test-Checkliste
#### Rechnungen (Bestand - sollte weiter funktionieren)
- [ ] Section erstellen
- [ ] Produkte zur Section hinzufügen
- [ ] Textzeilen erstellen
- [ ] Zwischensummen anzeigen
- [ ] Drag & Drop
- [ ] Sync zu facturedet
#### Angebote (NEU)
- [ ] Section erstellen
- [ ] Produkte zur Section hinzufügen
- [ ] Textzeilen erstellen
- [ ] Zwischensummen anzeigen
- [ ] Drag & Drop
#### Kundenaufträge (NEU)
- [ ] Section erstellen
- [ ] Produkte zur Section hinzufügen
- [ ] Textzeilen erstellen
- [ ] Zwischensummen anzeigen
- [ ] Drag & Drop
## Bekannte Probleme / TODOs
1. **AJAX-Dateien noch nicht angepasst** - Alle AJAX-Calls verwenden noch `facture_id` statt generischem `document_id`
2. **JavaScript teilweise angepasst** - Viele Funktionen verwenden noch `facture_id` statt `document_id` + `document_type`
3. **Sync-Funktionalität** - Die Sync-zu-PDF-Funktionalität (`in_facturedet`) muss für Angebote und Aufträge getestet werden
4. **PDF-Templates** - Eventuell müssen auch PDF-Templates angepasst werden
5. **Substitutions** - Die Substitutions-Funktionen müssen eventuell erweitert werden
## Nächste Schritte (Priorität)
1. **AJAX-Dateien anpassen** (HOCH)
- Template-Beispiel erstellen
- Alle AJAX-Dateien nach Template anpassen
2. **JavaScript vollständig generisch machen** (HOCH)
- `facture_id` durch `document_id` ersetzen
- `document_type` überall hinzufügen
3. **Testen** (MITTEL)
- Mit Angeboten testen
- Mit Kundenaufträgen testen
4. **renderAllPendingSections überprüfen** (MITTEL)
- SQL-Queries auf Korrektheit prüfen
- Alle `$doc_key` vs `$document_id` Verwendungen überprüfen
## Support
Bei Fragen oder Problemen:
- Dokumentation: `/srv/http/dolibarr/custom/subtotaltitle/README.md`
- Code-Review empfohlen für: `class/actions_subtotaltitle.class.php`
---
**Version:** 1.1.0
**Datum:** 2026-01-23
**Autor:** Eduard Wisch

241
README.md Normal file → Executable file
View file

@ -1,88 +1,205 @@
# SubtotalTitle - Facturedet Sync Update
# SubtotalTitle - Dolibarr Modul
## Was ist neu?
- **📄 Checkbox** bei jeder Section/Textzeile/Subtotal: Element zur Rechnung hinzufügen
- **→ Zur Rechnung / ← Aus Rechnung** Buttons: Alle Elemente auf einmal
- **ODT-Variablen** für formatierte Ausgabe im PDF/ODT
Erweitert Rechnungen, Angebote und Kundenaufträge um **Sections**, **Textzeilen** und **Zwischensummen**.
## Installation
---
### 1. Dateien kopieren
## 🔑 ODT Template Schlüsselwörter
### Zeilen-Variablen (pro Zeile in row.lines)
| Variable | Wert | Beschreibung |
|----------|------|--------------|
| `{line_is_section}` | 1/"" | Zeile ist eine Section (Überschrift) |
| `{line_is_textline}` | 1/"" | Zeile ist eine Textzeile |
| `{line_is_subtotal}` | 1/"" | Zeile ist eine Zwischensumme |
| `{line_is_product}` | 1/"" | Zeile ist ein Produkt mit Produktreferenz |
| `{line_is_free_line}` | 1/"" | Zeile ist eine freie Zeile (ohne Produktreferenz) |
| `{line_is_normal}` | 1/"" | Zeile ist normal (special_code = 0) |
| `{line_is_special}` | 1/"" | Zeile ist Section, Text ODER Subtotal |
| `{line_special_code}` | 0-102 | special_code Wert der Zeile |
### Globale Variablen (für das gesamte Dokument)
| Variable | Wert | Beschreibung |
|----------|------|--------------|
| `{object_has_sections}` | 1/"" | Dokument enthält mindestens eine Section |
| `{object_has_textlines}` | 1/"" | Dokument enthält mindestens eine Textzeile |
| `{object_has_speciallines}` | 1/"" | Dokument enthält Sections, Textzeilen oder Subtotals |
| `{object_count_sections}` | Zahl | Anzahl der Sections im Dokument |
| `{object_count_textlines}` | Zahl | Anzahl der Textzeilen im Dokument |
| `{object_count_subtotals}` | Zahl | Anzahl der Zwischensummen im Dokument |
### special_code Werte
| Typ | special_code |
|-----|-------------|
| Normales Produkt | 0 |
| Section (Überschrift) | 100 |
| Textzeile | 101 |
| Zwischensumme | 102 |
### ODT Template Beispiel
```
subtotaltitle_complete/
├── class/
│ └── actions_subtotaltitle.class.php → ERSETZEN
├── ajax/
│ └── sync_to_facturedet.php → NEU
├── js/
│ └── subtotaltitle_sync.js → NEU
├── css/
│ └── subtotaltitle_sync.css → Inhalt zu deiner CSS hinzufügen
└── core/
└── substitutions/
└── functions_subtotaltitle.lib.php → NEU (Ordner ggf. erstellen!)
```
[!-- IF {object_has_sections} --]
Dieses Dokument enthält {object_count_sections} Section(s).
[!-- ENDIF {object_has_sections} --]
### 2. Modul-Descriptor anpassen
In `core/modules/modSubtotalTitle.class.php` ändern:
```php
$this->module_parts = array(
'substitutions' => 1, // ← Diese Zeile hinzufügen/ändern!
'hooks' => array(
'data' => array('invoicecard'),
'entity' => '0'
)
);
```
### 3. Modul deaktivieren und wieder aktivieren
Damit die Substitution-Funktion erkannt wird.
## Verwendung
### In der Rechnungsansicht
| Element | Checkbox | Bedeutung |
|---------|----------|-----------|
| Section | 📄 | Zur Rechnung hinzufügen |
| Textzeile | 📄 | Zur Rechnung hinzufügen |
| Subtotal | 📄 | Zur Rechnung hinzufügen |
**Grüner Rand** = Element ist in der Rechnung/PDF enthalten
### Im ODT-Template
```
[!-- BEGIN row.lines --]
[!-- IF {line_is_section} --]
═══════════════════════════════════════
{line_desc}
═══════════════════════════════════════
[!-- ENDIF {line_is_section} --]
[!-- IF {line_is_textline} --]
{line_desc}
[!-- ENDIF {line_is_textline} --]
[!-- IF {line_is_normal} --]
[!-- IF {line_is_product} --]
{line_pos} {line_qty} {line_ref} {line_desc} {line_up_locale} € {line_price_ht_locale} €
[!-- ENDIF {line_is_product} --]
[!-- IF {line_is_free_line} --]
{line_pos} {line_qty} {line_desc} {line_up_locale} € {line_price_ht_locale} €
[!-- ENDIF {line_is_normal} --]
[!-- ENDIF {line_is_free_line} --]
[!-- IF {line_is_subtotal} --]
───────────────────────────────────────
Zwischensumme: {line_price_ht_locale} €
───────────────────────────────────────
[!-- ENDIF {line_is_subtotal} --]
[!-- END row.lines --]
```
## special_code Werte
---
| Typ | special_code | ODT-Variable |
|-----|-------------|--------------|
| Normales Produkt | 0 | `{line_is_normal}` |
| Section | 100 | `{line_is_section}` |
| Textzeile | 101 | `{line_is_textline}` |
| Zwischensumme | 102 | `{line_is_subtotal}` |
## 📋 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
### 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)
---
## 🔧 Installation
### 1. Dateien kopieren
```
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'
)
);
```
---
## 🗄️ 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
Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
Dieses Programm ist freie Software: Sie können es unter den Bedingungen der GNU General Public License, wie von der Free Software Foundation veröffentlicht, weitergeben und/oder modifizieren, entweder Version 3 der Lizenz oder (nach Ihrer Wahl) jede spätere Version.

171
ajax/add_to_section.php Executable file
View file

@ -0,0 +1,171 @@
<?php
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$line_id = GETPOST('line_id', 'int');
$section_id = GETPOST('section_id', 'int');
$document_id = GETPOST('document_id', 'int');
$docType = GETPOST('document_type', 'alpha');
subtotaltitle_debug_log('🔗 add_to_section: line='.$line_id.', section='.$section_id.', doc='.$document_id.', docType='.$docType);
if (!$line_id || !$section_id || !$document_id || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit;
}
$db->begin();
// 1. Hole die Manager-Zeile des Produkts
$sql = "SELECT rowid, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_line']." = ".(int)$line_id;
$sql .= " AND ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'product'";
$resql = $db->query($sql);
if (!$resql || $db->num_rows($resql) == 0) {
$db->rollback();
echo json_encode(['success' => false, 'error' => 'Product not found in manager table']);
exit;
}
$product = $db->fetch_object($resql);
$manager_id = $product->rowid;
$current_line_order = $product->line_order;
// 2. Hole die line_order der Section
$sql_section = "SELECT line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_section .= " WHERE rowid = ".(int)$section_id;
$sql_section .= " AND line_type = 'section'";
$sql_section .= " AND document_type = '".$db->escape($docType)."'";
$resql_section = $db->query($sql_section);
if (!$resql_section || $db->num_rows($resql_section) == 0) {
$db->rollback();
echo json_encode(['success' => false, 'error' => 'Section not found']);
exit;
}
$section = $db->fetch_object($resql_section);
$section_line_order = $section->line_order;
// 3. Finde die neue line_order für das Produkt
// Das Produkt soll IMMER als LETZTES Produkt der Section eingefügt werden
// (nach allen bestehenden Produkten dieser Section, aber vor der nächsten Section oder dem Subtotal)
// Suche das letzte Produkt/Text dieser Section
$sql_last = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_last .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_last .= " AND document_type = '".$db->escape($docType)."'";
$sql_last .= " AND parent_section = ".(int)$section_id;
$sql_last .= " AND line_type IN ('product', 'text')";
$sql_last .= " AND rowid != ".(int)$manager_id; // Nicht das aktuelle Produkt selbst
$resql_last = $db->query($sql_last);
$obj_last = $db->fetch_object($resql_last);
if ($obj_last && $obj_last->max_order) {
// Es gibt bereits Produkte in dieser Section → füge NACH dem letzten ein
$new_line_order = (int)$obj_last->max_order + 1;
} else {
// Keine anderen Produkte in der Section → füge direkt nach der Section ein
$new_line_order = $section_line_order + 1;
}
subtotaltitle_debug_log(' current_line_order='.$current_line_order.', section_line_order='.$section_line_order.', new_line_order='.$new_line_order);
// 4. Verschiebe Zeilen
// WICHTIG: Zuerst das Produkt "entfernen" (temporär auf -1 setzen), dann verschieben, dann einfügen
$sql_temp = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_temp .= " SET line_order = -1";
$sql_temp .= " WHERE rowid = ".(int)$manager_id;
$db->query($sql_temp);
if ($current_line_order < $new_line_order) {
// Produkt wird nach hinten verschoben
// Schließe die Lücke: alle Zeilen zwischen current+1 und new_line_order um -1 verschieben
$sql_shift = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_shift .= " SET line_order = line_order - 1";
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_shift .= " AND document_type = '".$db->escape($docType)."'";
$sql_shift .= " AND line_order > ".(int)$current_line_order;
$sql_shift .= " AND line_order <= ".(int)$new_line_order;
$db->query($sql_shift);
// Korrigiere new_line_order (weil wir verschoben haben)
$new_line_order = $new_line_order - 1;
subtotaltitle_debug_log(' Nach hinten: Lücke geschlossen, new_line_order korrigiert auf '.$new_line_order);
} elseif ($current_line_order > $new_line_order) {
// Produkt wird nach vorne verschoben
// Mache Platz: alle Zeilen ab new_line_order bis current-1 um +1 verschieben
$sql_shift = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_shift .= " SET line_order = line_order + 1";
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_shift .= " AND document_type = '".$db->escape($docType)."'";
$sql_shift .= " AND line_order >= ".(int)$new_line_order;
$sql_shift .= " AND line_order < ".(int)$current_line_order;
$db->query($sql_shift);
subtotaltitle_debug_log(' Nach vorne: Platz gemacht ab position '.$new_line_order);
}
// 5. Update das Produkt: setze parent_section und neue line_order
$sql_update = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_update .= " SET parent_section = ".(int)$section_id;
$sql_update .= ", line_order = ".(int)$new_line_order;
$sql_update .= " WHERE rowid = ".(int)$manager_id;
$db->query($sql_update);
subtotaltitle_debug_log('✅ Produkt #'.$line_id.' zu Section #'.$section_id.' hinzugefügt mit line_order='.$new_line_order);
// 6. Normalisiere alle line_order (keine Lücken, keine Duplikate)
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " ORDER BY line_order, rowid"; // Bei Gleichstand nach rowid sortieren
$resql = $db->query($sql);
$new_order = 1;
while ($obj = $db->fetch_object($resql)) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_upd .= " SET line_order = ".$new_order;
$sql_upd .= " WHERE rowid = ".(int)$obj->rowid;
$db->query($sql_upd);
$new_order++;
}
subtotaltitle_debug_log(' line_order normalisiert: '.($new_order-1).' Zeilen');
// 7. Sync rang in Detail-Tabelle
subtotaltitle_debug_log(' Starte rang-Synchronisation für docType='.$docType.' doc_id='.$document_id);
$sql = "SELECT rowid, line_type, ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND ".$tables['fk_line']." IS NOT NULL";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$rang = 1;
while ($obj = $db->fetch_object($resql)) {
$detail_id = $obj->detail_id;
if ($detail_id) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_upd .= " SET rang = ".$rang;
$sql_upd .= " WHERE rowid = ".(int)$detail_id;
$db->query($sql_upd);
subtotaltitle_debug_log(' Sync rang: '.$obj->line_type.' manager#'.$obj->rowid.' detail#'.$detail_id.' → rang='.$rang);
$rang++;
}
}
subtotaltitle_debug_log(' rang-Synchronisation abgeschlossen, '.$rang.' Zeilen synchronisiert');
$db->commit();
echo json_encode(['success' => true]);

View file

@ -2,22 +2,31 @@
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$facture_id = GETPOST('facture_id', 'int');
$section_id = GETPOST('section_id', 'int');
$docType = GETPOST('document_type', 'alpha');
subtotaltitle_debug_log('🔍 assign_last_product: facture=' . $facture_id . ', section=' . $section_id);
subtotaltitle_debug_log('🔍 assign_last_product: facture=' . $facture_id . ', section=' . $section_id . ', docType=' . $docType);
if (!$facture_id || !$section_id) {
if (!$facture_id || !$section_id || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
// Hole die richtigen Tabellennamen für diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit;
}
$db->begin();
// Hole das neueste Produkt dieser Rechnung (höchster rang)
$sql = "SELECT rowid, rang FROM ".MAIN_DB_PREFIX."facturedet";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
// Hole das neueste Produkt dieses Dokuments (höchster rang)
$sql = "SELECT rowid, rang FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql .= " ORDER BY rang DESC LIMIT 1";
$resql = $db->query($sql);
@ -32,37 +41,67 @@ $product_id = $product->rowid;
subtotaltitle_debug_log(' → Neustes Produkt: #' . $product_id . ' (rang=' . $product->rang . ')');
// Prüfe ob schon in Manager-Tabelle
// Prüfe ob schon in Manager-Tabelle (anhand der Detail-FK-Spalte)
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facturedet = ".(int)$product_id;
$sql .= " WHERE ".$tables['fk_line']." = ".(int)$product_id;
$resql = $db->query($sql);
// Hole die line_order der Section (Produkt soll direkt danach kommen)
$sql_section = "SELECT line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE rowid = ".(int)$section_id;
$resql_section = $db->query($sql_section);
$section_order = 1;
if ($obj_section = $db->fetch_object($resql_section)) {
$section_order = $obj_section->line_order;
}
// Berechne neue line_order: Höchste line_order der Produkte in dieser Section + 1
// Oder Section line_order + 1 wenn keine Produkte vorhanden
$sql_max_in_section = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_max_in_section .= " WHERE parent_section = ".(int)$section_id;
$sql_max_in_section .= " AND line_type = 'product'";
$resql_max_section = $db->query($sql_max_in_section);
$obj_max_section = $db->fetch_object($resql_max_section);
if ($obj_max_section && $obj_max_section->max_order) {
$new_line_order = $obj_max_section->max_order + 1;
} else {
$new_line_order = $section_order + 1;
}
subtotaltitle_debug_log(' → Section line_order='.$section_order.', neue Produkt line_order='.$new_line_order);
// Verschiebe alle nachfolgenden Zeilen um 1 nach hinten
$sql_shift = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_shift .= " SET line_order = line_order + 1";
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_shift .= " AND document_type = '".$db->escape($docType)."'";
$sql_shift .= " AND line_order >= ".$new_line_order;
$db->query($sql_shift);
if ($db->num_rows($resql) == 0) {
// Produkt fehlt - hinzufügen
$next_order = 1;
$sql_max = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_max .= " WHERE fk_facture = ".(int)$facture_id;
$resql_max = $db->query($sql_max);
if ($obj = $db->fetch_object($resql_max)) {
$next_order = ($obj->max_order ? $obj->max_order + 1 : 1);
}
// Setze alle FK-Felder explizit (NULL für nicht genutzte)
$fk_facture = ($docType === 'invoice') ? (int)$facture_id : 'NULL';
$fk_propal = ($docType === 'propal') ? (int)$facture_id : 'NULL';
$fk_commande = ($docType === 'order') ? (int)$facture_id : 'NULL';
$sql_ins = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_ins .= " (fk_facture, line_type, fk_facturedet, parent_section, line_order, date_creation)";
$sql_ins .= " VALUES (".(int)$facture_id.", 'product', ".(int)$product_id.", ".(int)$section_id.", ".$next_order.", NOW())";
$sql_ins .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, ".$tables['fk_line'].", parent_section, line_order, date_creation)";
$sql_ins .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'product', ".(int)$product_id.", ".(int)$section_id.", ".$new_line_order.", NOW())";
$db->query($sql_ins);
subtotaltitle_debug_log(' → Produkt zu Manager-Tabelle hinzugefügt (line_order=' . $next_order . ')');
subtotaltitle_debug_log(' → Produkt zu Manager-Tabelle hinzugefügt (line_order=' . $new_line_order . ')');
} else {
// Produkt existiert - UPDATE parent_section
subtotaltitle_debug_log('🔵🔵🔵 assign_last_product: Produkt #'.$product_id.' → parent_section='.$section_id);
// Produkt existiert - UPDATE parent_section UND line_order
subtotaltitle_debug_log('🔵🔵🔵 assign_last_product: Produkt #'.$product_id.' → parent_section='.$section_id.', line_order='.$new_line_order);
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_upd .= " SET parent_section = ".(int)$section_id;
$sql_upd .= " WHERE fk_facturedet = ".(int)$product_id;
$sql_upd .= ", line_order = ".$new_line_order;
$sql_upd .= " WHERE ".$tables['fk_line']." = ".(int)$product_id;
$db->query($sql_upd);
subtotaltitle_debug_log(' → parent_section updated');
subtotaltitle_debug_log(' → parent_section und line_order updated');
}
// Neu sortieren

48
ajax/check_subtotal.php Executable file
View file

@ -0,0 +1,48 @@
<?php
/**
* Prüft ob ein Subtotal für eine Section existiert
*/
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$section_id = GETPOST('section_id', 'int');
$docType = GETPOST('document_type', 'alpha');
if (!$section_id || !$docType) {
echo json_encode(['exists' => false, 'error' => 'Missing parameters']);
exit;
}
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['exists' => false, 'error' => 'Invalid document type']);
exit;
}
// Prüfe ob Subtotal in Manager-Tabelle existiert
$sql = "SELECT rowid, ".$tables['fk_line']." as detail_id, in_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE parent_section = ".(int)$section_id;
$sql .= " AND line_type = 'subtotal'";
$sql .= " AND document_type = '".$db->escape($docType)."'";
$resql = $db->query($sql);
$exists = false;
$subtotal_id = null;
$detail_id = null;
$in_facturedet = false;
if ($resql && $db->num_rows($resql) > 0) {
$obj = $db->fetch_object($resql);
$exists = true;
$subtotal_id = $obj->rowid;
$detail_id = $obj->detail_id;
$in_facturedet = $obj->in_facturedet ? true : false;
}
echo json_encode([
'exists' => $exists,
'subtotal_id' => $subtotal_id,
'detail_id' => $detail_id,
'in_facturedet' => $in_facturedet
]);

144
ajax/cleanup_subtotals.php Executable file
View file

@ -0,0 +1,144 @@
<?php
/**
* Bereinigt verwaiste Subtotals und fehlerhafte Einträge
*/
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$facture_id = GETPOST('facture_id', 'int');
$docType = GETPOST('document_type', 'alpha');
if (!$facture_id || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit;
}
$db->begin();
$deleted = 0;
$fixed = 0;
// 0. Sections dürfen KEINE parent_section haben - korrigiere das zuerst
$sql_fix_sections = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_fix_sections .= " SET parent_section = NULL";
$sql_fix_sections .= " WHERE line_type = 'section'";
$sql_fix_sections .= " AND parent_section IS NOT NULL";
$sql_fix_sections .= " AND ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_fix_sections .= " AND document_type = '".$db->escape($docType)."'";
$resql_fix = $db->query($sql_fix_sections);
$sections_fixed = $resql_fix ? $db->affected_rows($resql_fix) : 0;
if ($sections_fixed > 0) {
subtotaltitle_debug_log('🧹 ' . $sections_fixed . ' Sections mit falscher parent_section korrigiert');
$fixed += $sections_fixed;
}
// 0b. parent_section = 0 sollte NULL sein
$sql_fix_zero = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_fix_zero .= " SET parent_section = NULL";
$sql_fix_zero .= " WHERE parent_section = 0";
$sql_fix_zero .= " AND ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_fix_zero .= " AND document_type = '".$db->escape($docType)."'";
$resql_fix_zero = $db->query($sql_fix_zero);
$zero_fixed = $resql_fix_zero ? $db->affected_rows($resql_fix_zero) : 0;
if ($zero_fixed > 0) {
subtotaltitle_debug_log('🧹 ' . $zero_fixed . ' Einträge mit parent_section=0 korrigiert');
$fixed += $zero_fixed;
}
// 1. Lösche fehlerhafte "Produkte" in der Detail-Tabelle die eigentlich Zwischensummen sind
// (erkennbar an description LIKE 'Zwischensumme%' aber OHNE special_code 102)
$sql_bad = "SELECT rowid FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_bad .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_bad .= " AND description LIKE 'Zwischensumme%'";
$sql_bad .= " AND (special_code IS NULL OR special_code != 102)";
$resql_bad = $db->query($sql_bad);
while ($obj = $db->fetch_object($resql_bad)) {
// Lösche aus Detail-Tabelle
$sql_del = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".(int)$obj->rowid;
$db->query($sql_del);
// Lösche auch aus Manager-Tabelle falls vorhanden
$sql_del_mgr = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_del_mgr .= " WHERE ".$tables['fk_line']." = ".(int)$obj->rowid;
$db->query($sql_del_mgr);
subtotaltitle_debug_log('🧹 Fehlerhaftes Zwischensummen-Produkt gelöscht: #' . $obj->rowid);
$fixed++;
}
// 2. Lösche Subtotals deren Section show_subtotal = 0 hat
$sql = "SELECT sub.rowid, sub.".$tables['fk_line']." as detail_id";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager sub";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."facture_lines_manager sec ON sec.rowid = sub.parent_section";
$sql .= " WHERE sub.line_type = 'subtotal'";
$sql .= " AND sub.".$tables['fk_parent']." = ".(int)$facture_id;
$sql .= " AND sub.document_type = '".$db->escape($docType)."'";
$sql .= " AND (sec.show_subtotal = 0 OR sec.show_subtotal IS NULL)";
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
// Auch aus Detail-Tabelle löschen falls vorhanden
if ($obj->detail_id) {
$sql_del_det = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".(int)$obj->detail_id;
$db->query($sql_del_det);
}
// Aus Manager löschen
$sql_del = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE rowid = ".(int)$obj->rowid;
$db->query($sql_del);
$deleted++;
}
if ($deleted > 0 || $fixed > 0) {
subtotaltitle_debug_log('🧹 Cleanup: ' . $deleted . ' verwaiste Subtotals, ' . $fixed . ' fehlerhafte Produkte gelöscht');
// line_order neu durchnummerieren
$sql_reorder = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_reorder .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_reorder .= " AND document_type = '".$db->escape($docType)."'";
$sql_reorder .= " ORDER BY line_order";
$resql = $db->query($sql_reorder);
$new_order = 1;
while ($obj = $db->fetch_object($resql)) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_upd .= " SET line_order = ".$new_order;
$sql_upd .= " WHERE rowid = ".(int)$obj->rowid;
$db->query($sql_upd);
$new_order++;
}
// Auch rang in Detail-Tabelle neu durchnummerieren
$sql_sync = "SELECT ".$tables['fk_line']." FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_sync .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_sync .= " AND document_type = '".$db->escape($docType)."'";
$sql_sync .= " AND ".$tables['fk_line']." IS NOT NULL";
$sql_sync .= " ORDER BY line_order";
$resql_sync = $db->query($sql_sync);
$rang = 1;
while ($obj = $db->fetch_object($resql_sync)) {
$fk_line_value = $obj->{$tables['fk_line']};
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_upd .= " SET rang = ".$rang;
$sql_upd .= " WHERE rowid = ".(int)$fk_line_value;
$db->query($sql_upd);
$rang++;
}
}
$db->commit();
echo json_encode([
'success' => true,
'deleted' => $deleted,
'fixed' => $fixed
]);

View file

@ -1,30 +1,49 @@
<?php
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$facture_id = GETPOST('facture_id', 'int');
$title = GETPOST('title', 'alpha');
$docType = GETPOST('document_type', 'alpha'); // NEU!
if (!$facture_id || !$title) {
if (!$facture_id || !$title || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
// Hole die richtigen Tabellennamen für diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit;
}
// Hole nächste line_order
$sql = "SELECT MAX(line_order) as max_order";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$resql = $db->query($sql);
$obj = $db->fetch_object($resql);
$next_order = ($obj->max_order ? $obj->max_order + 1 : 1);
$next_order = ($obj && $obj->max_order ? $obj->max_order + 1 : 1);
// Erstelle Section - setze alle FK-Felder explizit (NULL für nicht genutzte)
$fk_facture = ($docType === 'invoice') ? (int)$facture_id : 'NULL';
$fk_propal = ($docType === 'propal') ? (int)$facture_id : 'NULL';
$fk_commande = ($docType === 'order') ? (int)$facture_id : 'NULL';
// Erstelle Section
$sql = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " (fk_facture, line_type, title, line_order, date_creation)";
$sql .= " VALUES (".(int)$facture_id.", 'section', '".$db->escape($title)."', ".$next_order.", NOW())";
$sql .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title, line_order, date_creation)";
$sql .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'section', '".$db->escape($title)."', ".$next_order.", NOW())";
if ($db->query($sql)) {
echo json_encode(['success' => true, 'section_id' => $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager")]);
$section_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");
// KEIN automatisches Subtotal mehr - wird nur erstellt wenn Checkbox aktiviert wird
// Das Subtotal wird über toggle_subtotal.php erstellt/gelöscht
echo json_encode(['success' => true, 'section_id' => $section_id]);
} else {
echo json_encode(['success' => false, 'error' => $db->lasterror()]);
}

View file

@ -6,27 +6,42 @@ if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../mai
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
if (!$res) die("Include of main fails");
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
header('Content-Type: application/json');
$facture_id = GETPOST('facture_id', 'int');
$text = GETPOST('text', 'restricthtml');
$docType = GETPOST('document_type', 'alpha');
if (!$facture_id || !$text) {
if (!$facture_id || !$text || !$docType) {
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
exit;
}
// Hole die richtigen Tabellennamen für diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(array('success' => false, 'error' => 'Invalid document type'));
exit;
}
// Hole nächste line_order
$sql = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$resql = $db->query($sql);
$obj = $db->fetch_object($resql);
$next_order = ($obj->max_order ? $obj->max_order + 1 : 1);
// Füge Textzeile ein
// Füge Textzeile ein - setze alle FK-Felder explizit (NULL für nicht genutzte)
$fk_facture = ($docType === 'invoice') ? (int)$facture_id : 'NULL';
$fk_propal = ($docType === 'propal') ? (int)$facture_id : 'NULL';
$fk_commande = ($docType === 'order') ? (int)$facture_id : 'NULL';
$sql = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " (fk_facture, line_type, title, line_order, date_creation)";
$sql .= " VALUES (".(int)$facture_id.", 'text', '".$db->escape($text)."', ".$next_order.", NOW())";
$sql .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title, line_order, in_facturedet, date_creation)";
$sql .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'text', '".$db->escape($text)."', ".$next_order.", 0, NOW())";
if ($db->query($sql)) {
$new_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");

View file

@ -1,23 +1,31 @@
<?php
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
global $user;
$section_id = GETPOST('section_id', 'int');
$force = GETPOST('force', 'int');
$docType = GETPOST('document_type', 'alpha');
subtotaltitle_debug_log('🔄 delete_section: section=' . $section_id . ', force=' . $force);
subtotaltitle_debug_log('delete_section: section=' . $section_id . ', force=' . $force . ', docType=' . $docType);
if (!$section_id) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
// Hole die richtigen Tabellennamen fuer diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit;
}
// 1. Hole Section-Info
$sql = "SELECT fk_facture FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql = "SELECT ".$tables['fk_parent']." as doc_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE rowid = ".(int)$section_id;
$sql .= " AND line_type = 'section'";
$resql = $db->query($sql);
@ -28,56 +36,62 @@ if (!$resql || $db->num_rows($resql) == 0) {
}
$section = $db->fetch_object($resql);
$facture_id = $section->fk_facture;
$document_id = $section->doc_id;
// 2. Prüfe Rechnungsstatus
$facture = new Facture($db);
$facture->fetch($facture_id);
// 2. Pruefe Dokumentstatus
$object = DocumentTypeHelper::loadDocument($docType, $document_id, $db);
if (!$object) {
echo json_encode(['success' => false, 'error' => 'Dokument nicht gefunden']);
exit;
}
if ($force && $facture->statut != Facture::STATUS_DRAFT) {
echo json_encode(['success' => false, 'error' => 'Rechnung ist nicht im Entwurf']);
$isDraft = DocumentTypeHelper::isDraft($object, $docType);
if ($force && !$isDraft) {
echo json_encode(['success' => false, 'error' => 'Dokument ist nicht im Entwurf']);
exit;
}
// 3. Hole Produkt-IDs DIREKT aus DB
$product_ids = [];
$sql_products = "SELECT fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_products = "SELECT ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_products .= " WHERE parent_section = ".(int)$section_id;
$sql_products .= " AND line_type = 'product'";
$res_products = $db->query($sql_products);
while ($prod = $db->fetch_object($res_products)) {
$product_ids[] = (int)$prod->fk_facturedet;
if ($prod->detail_id) {
$product_ids[] = (int)$prod->detail_id;
}
}
$product_count = count($product_ids);
subtotaltitle_debug_log('🔍 Gefundene Produkte in Section: ' . implode(', ', $product_ids));
subtotaltitle_debug_log('Gefundene Produkte in Section: ' . implode(', ', $product_ids));
$db->begin();
// 4. Force-Delete: Produkte aus Rechnung löschen
// 4. Force-Delete: Produkte aus Dokument loeschen
if ($force && $product_count > 0) {
subtotaltitle_debug_log('🗑️ Lösche ' . $product_count . ' Zeilen aus Rechnung...');
subtotaltitle_debug_log('Loesche ' . $product_count . ' Zeilen aus Dokument...');
foreach ($product_ids as $line_id) {
$sql_del_line = "DELETE FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid = ".(int)$line_id;
$sql_del_line = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".(int)$line_id;
$res_del = $db->query($sql_del_line);
if ($res_del) {
subtotaltitle_debug_log('✅ facturedet gelöscht: ' . $line_id);
subtotaltitle_debug_log('Detail geloescht: ' . $line_id);
} else {
subtotaltitle_debug_log('SQL Fehler: ' . $line_id . ' - ' . $db->lasterror());
subtotaltitle_debug_log('SQL Fehler: ' . $line_id . ' - ' . $db->lasterror());
}
}
// Aus Manager-Tabelle löschen
// Aus Manager-Tabelle loeschen
$sql_del = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_del .= " WHERE parent_section = ".(int)$section_id;
$sql_del .= " AND line_type = 'product'";
$db->query($sql_del);
subtotaltitle_debug_log('🔴 Force-Delete abgeschlossen: ' . $product_count . ' Produkte');
subtotaltitle_debug_log('Force-Delete abgeschlossen: ' . $product_count . ' Produkte');
} else if (!$force) {
// Ohne force: Produkte nur freigeben
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
@ -85,36 +99,35 @@ if ($force && $product_count > 0) {
$sql .= " WHERE parent_section = ".(int)$section_id;
$sql .= " AND line_type = 'product'";
$db->query($sql);
subtotaltitle_debug_log('🔓 ' . $product_count . ' Produkte freigegeben');
subtotaltitle_debug_log($product_count . ' Produkte freigegeben');
}
// ========== NEU: SUBTOTAL LÖSCHEN ==========
// Hole Subtotal dieser Section (falls vorhanden)
$sql_subtotal = "SELECT rowid, fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
// ========== SUBTOTAL LOESCHEN ==========
$sql_subtotal = "SELECT rowid, ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_subtotal .= " WHERE parent_section = ".(int)$section_id;
$sql_subtotal .= " AND line_type = 'subtotal'";
$res_subtotal = $db->query($sql_subtotal);
if ($obj_sub = $db->fetch_object($res_subtotal)) {
// Falls Subtotal in facturedet ist, dort auch löschen
if ($obj_sub->fk_facturedet > 0) {
$sql_del_fd = "DELETE FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid = ".(int)$obj_sub->fk_facturedet;
// Falls Subtotal in Detail-Tabelle ist, dort auch loeschen
if ($obj_sub->detail_id > 0) {
$sql_del_fd = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".(int)$obj_sub->detail_id;
$db->query($sql_del_fd);
subtotaltitle_debug_log('✅ Subtotal aus facturedet gelöscht: ' . $obj_sub->fk_facturedet);
subtotaltitle_debug_log('Subtotal aus Detail geloescht: ' . $obj_sub->detail_id);
}
// Aus Manager-Tabelle löschen
// Aus Manager-Tabelle loeschen
$sql_del_sub = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE rowid = ".(int)$obj_sub->rowid;
$db->query($sql_del_sub);
subtotaltitle_debug_log('✅ Subtotal aus Manager gelöscht: ' . $obj_sub->rowid);
subtotaltitle_debug_log('Subtotal aus Manager geloescht: ' . $obj_sub->rowid);
}
// ========== VERWAISTE SUBTOTALS AUFRÄUMEN ==========
// Finde alle Subtotals in dieser Rechnung, deren parent_section nicht mehr existiert
$sql_orphans = "SELECT s.rowid, s.fk_facturedet, s.parent_section
// ========== VERWAISTE SUBTOTALS AUFRAEUMEN ==========
$sql_orphans = "SELECT s.rowid, s.".$tables['fk_line']." as detail_id, s.parent_section
FROM ".MAIN_DB_PREFIX."facture_lines_manager s
WHERE s.fk_facture = ".(int)$facture_id."
WHERE s.".$tables['fk_parent']." = ".(int)$document_id."
AND s.document_type = '".$db->escape($docType)."'
AND s.line_type = 'subtotal'
AND s.parent_section IS NOT NULL
AND NOT EXISTS (
@ -126,36 +139,33 @@ $res_orphans = $db->query($sql_orphans);
$orphan_count = 0;
while ($orphan = $db->fetch_object($res_orphans)) {
// Aus facturedet löschen (falls vorhanden)
if ($orphan->fk_facturedet > 0) {
$sql_del_orphan_fd = "DELETE FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid = ".(int)$orphan->fk_facturedet;
if ($orphan->detail_id > 0) {
$sql_del_orphan_fd = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".(int)$orphan->detail_id;
$db->query($sql_del_orphan_fd);
subtotaltitle_debug_log('🧹 Verwaistes Subtotal aus facturedet gelöscht: ' . $orphan->fk_facturedet . ' (parent_section=' . $orphan->parent_section . ')');
subtotaltitle_debug_log('Verwaistes Subtotal aus Detail geloescht: ' . $orphan->detail_id);
}
// Aus Manager-Tabelle löschen
$sql_del_orphan = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE rowid = ".(int)$orphan->rowid;
$db->query($sql_del_orphan);
subtotaltitle_debug_log('🧹 Verwaistes Subtotal aus Manager gelöscht: ' . $orphan->rowid);
$orphan_count++;
}
if ($orphan_count > 0) {
subtotaltitle_debug_log('🧹 Aufgeräumt: ' . $orphan_count . ' verwaiste Subtotals entfernt');
subtotaltitle_debug_log('Aufgeraeumt: ' . $orphan_count . ' verwaiste Subtotals entfernt');
}
// ========== ENDE VERWAISTE SUBTOTALS ==========
// 5. Section selbst löschen
// 5. Section selbst loeschen
$sql = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE rowid = ".(int)$section_id;
$db->query($sql);
// Rechnungstotale neu berechnen (nach allen Löschungen)
$facture->update_price(1);
// Dokumenttotale neu berechnen
$object->update_price(1);
// 6. Neuordnen
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
@ -169,21 +179,24 @@ while ($obj = $db->fetch_object($resql)) {
}
// 7. Sync rang
$sql = "SELECT fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " AND line_type = 'product'";
$sql = "SELECT ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND ".$tables['fk_line']." IS NOT NULL";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$rang = 1;
while ($obj = $db->fetch_object($resql)) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facturedet";
$sql_upd .= " SET rang = ".$rang;
$sql_upd .= " WHERE rowid = ".(int)$obj->fk_facturedet;
$db->query($sql_upd);
$rang++;
if ($obj->detail_id) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_upd .= " SET rang = ".$rang;
$sql_upd .= " WHERE rowid = ".(int)$obj->detail_id;
$db->query($sql_upd);
$rang++;
}
}
$db->commit();
echo json_encode(['success' => true, 'deleted' => $force ? $product_count : 0]);
echo json_encode(['success' => true, 'deleted' => $force ? $product_count : 0]);

View file

@ -6,20 +6,42 @@ if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../mai
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
if (!$res) die("Include of main fails");
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
header('Content-Type: application/json');
$textline_id = GETPOST('textline_id', 'int');
$docType = GETPOST('document_type', 'alpha');
subtotaltitle_debug_log('🗑️ delete_textline: id=' . $textline_id);
// Fallback: Wenn kein docType, versuche aus der DB zu ermitteln
if (!$docType) {
$sql_type = "SELECT document_type FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE rowid = ".(int)$textline_id;
$res_type = $db->query($sql_type);
if ($res_type && $obj_type = $db->fetch_object($res_type)) {
$docType = $obj_type->document_type;
}
}
if (!$docType) {
$docType = 'invoice'; // Fallback
}
subtotaltitle_debug_log('delete_textline: id=' . $textline_id . ', docType=' . $docType);
if (!$textline_id) {
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
exit;
}
// 1. Hole facture_id BEVOR wir löschen
$sql = "SELECT fk_facture FROM ".MAIN_DB_PREFIX."facture_lines_manager";
// Hole die richtigen Tabellennamen fuer diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(array('success' => false, 'error' => 'Invalid document type'));
exit;
}
// 1. Hole document_id BEVOR wir loeschen
$sql = "SELECT ".$tables['fk_parent']." as doc_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE rowid = ".(int)$textline_id;
$resql = $db->query($sql);
@ -29,31 +51,54 @@ if (!$resql || $db->num_rows($resql) == 0) {
}
$obj = $db->fetch_object($resql);
$facture_id = $obj->fk_facture;
$document_id = $obj->doc_id;
// 2. DELETE ausführen
$db->begin();
// 2. DELETE ausfuehren
$sql = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE rowid = ".(int)$textline_id;
$sql .= " AND line_type = 'text'";
if (!$db->query($sql)) {
$db->rollback();
echo json_encode(array('success' => false, 'error' => $db->lasterror()));
exit;
}
// 3. Lücken schließen
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager
WHERE fk_facture = ".(int)$facture_id."
ORDER BY line_order";
// 3. line_order neu durchnummerieren
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$new_order = 1;
while ($obj = $db->fetch_object($resql)) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager
SET line_order = ".$new_order."
WHERE rowid = ".(int)$obj->rowid;
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager SET line_order = ".$new_order." WHERE rowid = ".(int)$obj->rowid;
$db->query($sql_upd);
$new_order++;
}
echo json_encode(array('success' => true));
// 4. rang in Detail-Tabelle synchronisieren
$sql = "SELECT ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND ".$tables['fk_line']." IS NOT NULL";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$rang = 1;
while ($obj = $db->fetch_object($resql)) {
if ($obj->detail_id) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table']." SET rang = ".$rang." WHERE rowid = ".(int)$obj->detail_id;
$db->query($sql_upd);
$rang++;
}
}
subtotaltitle_debug_log('delete_textline: rang synchronisiert, ' . ($rang - 1) . ' Zeilen');
$db->commit();
echo json_encode(array('success' => true));

View file

@ -6,26 +6,44 @@ if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../mai
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
if (!$res) die("Include of main fails");
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
header('Content-Type: application/json');
$textline_id = GETPOST('textline_id', 'int');
$text = GETPOST('text', 'restricthtml');
subtotaltitle_debug_log('🔄 edit_textline: id=' . $textline_id);
subtotaltitle_debug_log('edit_textline: id=' . $textline_id);
if (!$textline_id || !$text) {
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
exit;
}
// Hole erst fk_facturedet (falls Textzeile in Rechnung ist)
$sql_get = "SELECT fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
// Hole erst document_type und FK zur Detail-Tabelle
$sql_get = "SELECT document_type, fk_facturedet, fk_propaldet, fk_commandedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_get .= " WHERE rowid = ".(int)$textline_id;
$sql_get .= " AND line_type = 'text'";
$resql = $db->query($sql_get);
$obj = $db->fetch_object($resql);
$fk_facturedet = $obj ? $obj->fk_facturedet : null;
if (!$obj) {
echo json_encode(array('success' => false, 'error' => 'Textline not found'));
exit;
}
$docType = $obj->document_type ?: 'invoice';
$tables = DocumentTypeHelper::getTableNames($docType);
// Ermittle FK zur Detail-Tabelle basierend auf Dokumenttyp
$fk_detail = null;
if ($docType == 'invoice' && $obj->fk_facturedet > 0) {
$fk_detail = $obj->fk_facturedet;
} elseif ($docType == 'propal' && $obj->fk_propaldet > 0) {
$fk_detail = $obj->fk_propaldet;
} elseif ($docType == 'order' && $obj->fk_commandedet > 0) {
$fk_detail = $obj->fk_commandedet;
}
// Update Manager-Tabelle
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
@ -38,15 +56,15 @@ if (!$db->query($sql)) {
exit;
}
// Falls in facturedet vorhanden, dort auch updaten
if ($fk_facturedet > 0) {
$sql_fd = "UPDATE ".MAIN_DB_PREFIX."facturedet";
// Falls in Detail-Tabelle vorhanden, dort auch updaten
if ($fk_detail > 0 && $tables) {
$sql_fd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_fd .= " SET description = '".$db->escape($text)."'";
$sql_fd .= " WHERE rowid = ".(int)$fk_facturedet;
$sql_fd .= " WHERE rowid = ".(int)$fk_detail;
$db->query($sql_fd);
subtotaltitle_debug_log('✅ Textzeile + facturedet geändert');
subtotaltitle_debug_log('Textzeile + Detail geaendert (docType='.$docType.')');
} else {
subtotaltitle_debug_log('✅ Textzeile geändert (nicht in facturedet)');
subtotaltitle_debug_log('Textzeile geaendert (nicht in Detail-Tabelle)');
}
echo json_encode(array('success' => true, 'synced_facturedet' => ($fk_facturedet > 0)));
echo json_encode(array('success' => true, 'synced_detail' => ($fk_detail > 0)));

182
ajax/fix_section_hierarchy.php Executable file
View file

@ -0,0 +1,182 @@
<?php
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$doc_id = GETPOST('doc_id', 'int');
$docType = GETPOST('document_type', 'alpha');
if (!$doc_id || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit;
}
$db->begin();
echo "<h2>Repariere Section-Hierarchie und Sortierung</h2>";
// 1. Alle Sections haben parent_section=NULL (Sections können nicht in Sections sein!)
echo "<h3>1. Korrigiere Section parent_section Werte</h3>";
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " SET parent_section = NULL";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'section'";
$sql .= " AND parent_section IS NOT NULL";
$result = $db->query($sql);
echo "✅ Sections korrigiert (parent_section=NULL gesetzt)<br>";
// 2. Baue komplette neue line_order auf
echo "<h3>2. Neu-Sortierung aller Zeilen</h3>";
$new_order = 1;
$updates = array();
// Hole die line_order der ersten Section
$sql = "SELECT MIN(line_order) as first_section_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'section'";
$resql = $db->query($sql);
$obj = $db->fetch_object($resql);
$first_section_order = $obj ? $obj->first_section_order : 9999;
// Freie Produkte VOR den Sections
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'product'";
$sql .= " AND (parent_section IS NULL OR parent_section = 0)";
$sql .= " AND line_order < ".(int)$first_section_order;
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
$updates[$obj->rowid] = $new_order;
echo "Freies Produkt (VOR Sections) #".$obj->rowid." → line_order=".$new_order."<br>";
$new_order++;
}
// Alle Sections
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'section'";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$sections = array();
while ($obj = $db->fetch_object($resql)) {
$sections[] = $obj->rowid;
}
foreach ($sections as $sec_id) {
// Section selbst
$updates[$sec_id] = $new_order;
echo "<strong>Section #".$sec_id." → line_order=".$new_order."</strong><br>";
$new_order++;
// Produkte dieser Section
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'product'";
$sql .= " AND parent_section = ".(int)$sec_id;
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
$updates[$obj->rowid] = $new_order;
echo "&nbsp;&nbsp;→ Produkt #".$obj->rowid." → line_order=".$new_order."<br>";
$new_order++;
}
// Textzeilen dieser Section
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'text'";
$sql .= " AND parent_section = ".(int)$sec_id;
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
$updates[$obj->rowid] = $new_order;
echo "&nbsp;&nbsp;→ Text #".$obj->rowid." → line_order=".$new_order."<br>";
$new_order++;
}
// Subtotal dieser Section
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'subtotal'";
$sql .= " AND parent_section = ".(int)$sec_id;
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
$updates[$obj->rowid] = $new_order;
echo "&nbsp;&nbsp;→ Subtotal #".$obj->rowid." → line_order=".$new_order."<br>";
$new_order++;
}
}
// Freie Produkte NACH den Sections (alle, die noch nicht processed wurden)
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'product'";
$sql .= " AND (parent_section IS NULL OR parent_section = 0)";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
if (!isset($updates[$obj->rowid])) {
$updates[$obj->rowid] = $new_order;
echo "Freies Produkt (NACH Sections) #".$obj->rowid." → line_order=".$new_order."<br>";
$new_order++;
}
}
// 3. Updates ausführen
echo "<h3>3. Schreibe neue line_order Werte</h3>";
foreach ($updates as $rowid => $order) {
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " SET line_order = ".(int)$order;
$sql .= " WHERE rowid = ".(int)$rowid;
$db->query($sql);
}
echo "".count($updates)." Zeilen neu sortiert<br>";
// 4. Sync rang in Detail-Tabelle
echo "<h3>4. Synchronisiere rang in Detail-Tabelle</h3>";
$sql = "SELECT rowid, line_type, ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND ".$tables['fk_line']." IS NOT NULL";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$rang = 1;
while ($obj = $db->fetch_object($resql)) {
$detail_id = $obj->detail_id;
if ($detail_id) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_upd .= " SET rang = ".$rang;
$sql_upd .= " WHERE rowid = ".(int)$detail_id;
$db->query($sql_upd);
echo $obj->line_type." manager#".$obj->rowid." detail#".$detail_id." → rang=".$rang."<br>";
$rang++;
}
}
echo "".$rang." Zeilen synchronisiert<br>";
$db->commit();
echo "<h3>✅ Reparatur abgeschlossen!</h3>";
echo "<p><a href='/dolibarr/commande/card.php?id=".$doc_id."'>Zurück zum Kundenauftrag</a></p>";

58
ajax/fix_sections.php Executable file
View file

@ -0,0 +1,58 @@
<?php
/**
* Repariert fehlerhafte parent_section Werte
* - Sections sollten KEINE parent_section haben
* - parent_section = 0 sollte NULL sein
*/
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
$doc_id = GETPOST('doc_id', 'int');
$docType = GETPOST('document_type', 'alpha');
if (!$doc_id || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
$db->begin();
$fixed = 0;
// 1. Sections dürfen KEINE parent_section haben
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " SET parent_section = NULL";
$sql .= " WHERE line_type = 'section'";
$sql .= " AND parent_section IS NOT NULL";
$sql .= " AND document_type = '".$db->escape($docType)."'";
if ($docType == 'invoice') {
$sql .= " AND fk_facture = ".(int)$doc_id;
} elseif ($docType == 'propal') {
$sql .= " AND fk_propal = ".(int)$doc_id;
} elseif ($docType == 'order') {
$sql .= " AND fk_commande = ".(int)$doc_id;
}
$db->query($sql);
$fixed += $db->affected_rows();
// 2. parent_section = 0 sollte NULL sein
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " SET parent_section = NULL";
$sql .= " WHERE parent_section = 0";
$sql .= " AND document_type = '".$db->escape($docType)."'";
if ($docType == 'invoice') {
$sql .= " AND fk_facture = ".(int)$doc_id;
} elseif ($docType == 'propal') {
$sql .= " AND fk_propal = ".(int)$doc_id;
} elseif ($docType == 'order') {
$sql .= " AND fk_commande = ".(int)$doc_id;
}
$db->query($sql);
$fixed += $db->affected_rows();
$db->commit();
echo json_encode([
'success' => true,
'fixed' => $fixed,
'message' => $fixed . ' Einträge korrigiert'
]);

View file

@ -2,20 +2,30 @@
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$facture_id = GETPOST('facture_id', 'int');
subtotaltitle_debug_log('🔄 get_sections: facture=' . $facture_id);
$docType = GETPOST('document_type', 'alpha');
subtotaltitle_debug_log('🔄 get_sections: facture=' . $facture_id . ', docType=' . $docType);
if (!$facture_id) {
echo json_encode(array('success' => false, 'error' => 'Missing facture_id'));
if (!$facture_id || !$docType) {
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
exit;
}
// Hole ALLE Sections
// Hole die richtigen Tabellennamen für diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(array('success' => false, 'error' => 'Invalid document type'));
exit;
}
// Hole ALLE Sections für diesen Dokumenttyp
$sql = "SELECT s.rowid, s.title, s.line_order, ";
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."facture_lines_manager p WHERE p.parent_section = s.rowid) as product_count";
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."facture_lines_manager p WHERE p.parent_section = s.rowid AND p.line_type = 'product' AND p.document_type = '".$db->escape($docType)."') as product_count";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager s";
$sql .= " WHERE s.fk_facture = ".(int)$facture_id;
$sql .= " WHERE s.".$tables['fk_parent']." = ".(int)$facture_id;
$sql .= " AND s.document_type = '".$db->escape($docType)."'";
$sql .= " AND s.line_type = 'section'";
$sql .= " ORDER BY s.line_order";
$resql = $db->query($sql);

View file

@ -6,17 +6,28 @@ if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../mai
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
if (!$res) die("Include of main fails");
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
header('Content-Type: application/json');
$facture_id = GETPOST('facture_id', 'int');
$docType = GETPOST('document_type', 'alpha');
if (!$facture_id) {
echo json_encode(array('success' => false, 'error' => 'Missing facture_id'));
if (!$facture_id || !$docType) {
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
exit;
}
// Hole die richtigen Tabellennamen für diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(array('success' => false, 'error' => 'Invalid document type'));
exit;
}
$sql = "SELECT rowid, title, line_order, parent_section FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'text'";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);

565
ajax/import_from_origin.php Normal file
View file

@ -0,0 +1,565 @@
<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* Import sections/textlines from origin document (Angebot→Auftrag→Rechnung)
*/
define('NOTOKENRENEWAL', 1);
$res = 0;
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
if (!$res) die("Include of main fails");
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
require_once DOL_DOCUMENT_ROOT.'/comm/propal/class/propal.class.php';
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
dol_include_once('/subtotaltitle/lib/subtotaltitle.lib.php');
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
header('Content-Type: application/json');
$action = GETPOST('action', 'alpha');
$target_id = GETPOST('target_id', 'int');
$target_type = GETPOST('target_type', 'alpha');
subtotaltitle_debug_log('📥 import_from_origin: action='.$action.', target_id='.$target_id.', target_type='.$target_type);
if (!$target_id || !$target_type) {
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
exit;
}
// Hole die richtigen Tabellennamen für Ziel-Dokumenttyp
$target_tables = DocumentTypeHelper::getTableNames($target_type);
if (!$target_tables) {
echo json_encode(array('success' => false, 'error' => 'Invalid target document type'));
exit;
}
/**
* Ermittelt das Ursprungsdokument basierend auf Zieldokument
* Dolibarr speichert die Herkunft in origin/origin_id ODER in llx_element_element
*/
function getOriginDocument($db, $target_id, $target_type)
{
$target_tables = DocumentTypeHelper::getTableNames($target_type);
if (!$target_tables) {
return null;
}
// Lade Zieldokument
$target_doc = DocumentTypeHelper::loadDocument($target_type, $target_id, $db);
if (!$target_doc) {
subtotaltitle_debug_log('❌ Zieldokument nicht gefunden: '.$target_type.' #'.$target_id);
return null;
}
subtotaltitle_debug_log('🔍 Zieldokument geladen: element='.$target_doc->element.', origin='.($target_doc->origin ?? 'NULL').', origin_id='.($target_doc->origin_id ?? 'NULL'));
// Methode 1: Direkte Objekteigenschaften prüfen
$origin = $target_doc->origin ?? null;
$origin_id = $target_doc->origin_id ?? null;
// Methode 2: Falls nicht gesetzt, prüfe llx_element_element Tabelle
if (empty($origin) || empty($origin_id)) {
$elementType = $target_doc->element; // z.B. 'commande', 'facture', 'propal'
subtotaltitle_debug_log('🔍 Suche in element_element für '.$elementType.' #'.$target_id);
$sql_origin = "SELECT fk_source, sourcetype FROM ".MAIN_DB_PREFIX."element_element";
$sql_origin .= " WHERE fk_target = ".(int)$target_id;
$sql_origin .= " AND targettype = '".$db->escape($elementType)."'";
$sql_origin .= " LIMIT 1";
subtotaltitle_debug_log('SQL: '.$sql_origin);
$res_origin = $db->query($sql_origin);
if ($res_origin && $db->num_rows($res_origin) > 0) {
$obj_origin = $db->fetch_object($res_origin);
$origin = $obj_origin->sourcetype;
$origin_id = $obj_origin->fk_source;
subtotaltitle_debug_log('✅ Gefunden in element_element: '.$origin.' #'.$origin_id);
} else {
subtotaltitle_debug_log('❌ Kein Eintrag in element_element gefunden');
}
}
// Prüfe ob origin gesetzt ist
if (empty($origin) || empty($origin_id)) {
subtotaltitle_debug_log('❌ Kein Ursprungsdokument verknüpft (weder direkt noch in element_element)');
return null;
}
// Mappe Dolibarr origin zu unserem document_type
$origin_type_map = array(
'propal' => 'propal',
'commande' => 'order',
'facture' => 'invoice',
'order_supplier' => null, // Lieferantenauftrag - nicht unterstützt
'invoice_supplier' => null // Lieferantenrechnung - nicht unterstützt
);
$origin_type = isset($origin_type_map[$origin]) ? $origin_type_map[$origin] : null;
if (!$origin_type) {
subtotaltitle_debug_log('❌ Nicht unterstützter Ursprungstyp: '.$origin);
return null;
}
// Lade Ursprungsdokument
$origin_doc = DocumentTypeHelper::loadDocument($origin_type, $origin_id, $db);
if (!$origin_doc) {
subtotaltitle_debug_log('❌ Ursprungsdokument nicht gefunden: '.$origin_type.' #'.$origin_id);
return null;
}
subtotaltitle_debug_log('✅ Ursprungsdokument gefunden: '.$origin_type.' #'.$origin_id);
return array(
'document' => $origin_doc,
'type' => $origin_type,
'id' => $origin_id
);
}
/**
* Sucht die passende Produktzeile im Zieldokument basierend auf fk_product
*/
function findMatchingProductLine($db, $target_id, $target_type, $source_product_id)
{
if (!$source_product_id) {
return null;
}
$target_tables = DocumentTypeHelper::getTableNames($target_type);
// Suche nach Zeile mit gleichem Produkt im Zieldokument
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX.$target_tables['lines_table'];
$sql .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id;
$sql .= " AND fk_product = ".(int)$source_product_id;
$sql .= " LIMIT 1";
$resql = $db->query($sql);
if ($resql && $db->num_rows($resql) > 0) {
$obj = $db->fetch_object($resql);
return $obj->rowid;
}
return null;
}
if ($action == 'check') {
// ========== PRÜFE OB IMPORT MÖGLICH IST ==========
$origin = getOriginDocument($db, $target_id, $target_type);
if (!$origin) {
echo json_encode(array(
'success' => true,
'has_origin' => false,
'message' => 'Kein Ursprungsdokument verknüpft'
));
exit;
}
$origin_tables = DocumentTypeHelper::getTableNames($origin['type']);
// Zähle Sections und Textlines im Ursprungsdokument
$sql = "SELECT COUNT(*) as cnt, line_type FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$origin_tables['fk_parent']." = ".(int)$origin['id'];
$sql .= " AND document_type = '".$db->escape($origin['type'])."'";
$sql .= " AND line_type IN ('section', 'text')";
$sql .= " GROUP BY line_type";
$resql = $db->query($sql);
$counts = array('section' => 0, 'text' => 0);
while ($obj = $db->fetch_object($resql)) {
$counts[$obj->line_type] = $obj->cnt;
}
// Prüfe ob im Zieldokument schon Sections existieren
$sql_target = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_target .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id;
$sql_target .= " AND document_type = '".$db->escape($target_type)."'";
$sql_target .= " AND line_type = 'section'";
$res_target = $db->query($sql_target);
$obj_target = $db->fetch_object($res_target);
$has_existing = ($obj_target && $obj_target->cnt > 0);
// Ermittle Anzeigename für Ursprungsdokument
$origin_name = '';
$origin_ref = $origin['document']->ref;
switch ($origin['type']) {
case 'propal':
$origin_name = 'Angebot '.$origin_ref;
break;
case 'order':
$origin_name = 'Auftrag '.$origin_ref;
break;
case 'invoice':
$origin_name = 'Rechnung '.$origin_ref;
break;
}
echo json_encode(array(
'success' => true,
'has_origin' => true,
'origin_type' => $origin['type'],
'origin_id' => $origin['id'],
'origin_ref' => $origin_ref,
'origin_name' => $origin_name,
'sections_count' => (int)$counts['section'],
'textlines_count' => (int)$counts['text'],
'has_existing' => $has_existing,
'can_import' => ($counts['section'] > 0 || $counts['text'] > 0)
));
} elseif ($action == 'import') {
// ========== KOMPLETTER IMPORT MIT RANG-SYNCHRONISATION ==========
// Strategie:
// 1. Lösche bestehende Einträge in Manager-Tabelle für Zieldokument
// 2. Importiere alle Sections/Textlines/Subtotals aus Ursprung
// 3. Synchronisiere ALLE Produkte aus Ziel-Dolibarr-Tabelle in Manager-Tabelle
// 4. Ordne Produkte den Sections zu basierend auf fk_product Matching
// 5. Produkte die NUR im Zieldokument sind, kommen am Ende
// 6. Neu-Nummerierung line_order UND rang in beiden Tabellen
$origin = getOriginDocument($db, $target_id, $target_type);
if (!$origin) {
echo json_encode(array('success' => false, 'error' => 'Kein Ursprungsdokument gefunden'));
exit;
}
$origin_tables = DocumentTypeHelper::getTableNames($origin['type']);
// Starte Transaktion
$db->begin();
$imported_sections = 0;
$imported_textlines = 0;
$imported_subtotals = 0;
$product_assignments = 0;
$new_products = 0;
$section_mapping = array(); // Alte Section-ID => Neue Section-ID
try {
// ============================================================
// SCHRITT 1: Lösche bestehende Einträge in Manager-Tabelle
// ============================================================
subtotaltitle_debug_log('🗑️ Lösche bestehende Manager-Einträge...');
$sql_delete = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_delete .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id;
$sql_delete .= " AND document_type = '".$db->escape($target_type)."'";
$db->query($sql_delete);
subtotaltitle_debug_log('✅ Bestehende Einträge gelöscht');
// FK-Werte für Zieldokument
$fk_facture = ($target_type === 'invoice') ? (int)$target_id : 'NULL';
$fk_propal = ($target_type === 'propal') ? (int)$target_id : 'NULL';
$fk_commande = ($target_type === 'order') ? (int)$target_id : 'NULL';
// ============================================================
// SCHRITT 2: Baue Mapping fk_product → Section aus Ursprung
// ============================================================
subtotaltitle_debug_log('🗺️ Erstelle fk_product → Section Mapping...');
$product_section_map = array(); // fk_product => origin_section_id
$sql_origin_products = "SELECT m.parent_section, d.fk_product FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql_origin_products .= " LEFT JOIN ".MAIN_DB_PREFIX.$origin_tables['lines_table']." d ON d.rowid = m.".$origin_tables['fk_line'];
$sql_origin_products .= " WHERE m.".$origin_tables['fk_parent']." = ".(int)$origin['id'];
$sql_origin_products .= " AND m.document_type = '".$db->escape($origin['type'])."'";
$sql_origin_products .= " AND m.line_type = 'product'";
$sql_origin_products .= " AND m.parent_section IS NOT NULL";
$sql_origin_products .= " AND d.fk_product IS NOT NULL";
$res_origin_products = $db->query($sql_origin_products);
while ($row = $db->fetch_object($res_origin_products)) {
$product_section_map[$row->fk_product] = $row->parent_section;
subtotaltitle_debug_log(' Mapping: fk_product='.$row->fk_product.' → Section #'.$row->parent_section);
}
subtotaltitle_debug_log('✅ '.count($product_section_map).' Produkt-Section Mappings erstellt');
// ============================================================
// SCHRITT 3: Hole ALLE Einträge aus Ursprung (sortiert nach line_order)
// ============================================================
subtotaltitle_debug_log('📦 Hole alle Einträge aus Ursprungsdokument...');
$sql_origin_all = "SELECT m.*, d.fk_product FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql_origin_all .= " LEFT JOIN ".MAIN_DB_PREFIX.$origin_tables['lines_table']." d ON d.rowid = m.".$origin_tables['fk_line'];
$sql_origin_all .= " WHERE m.".$origin_tables['fk_parent']." = ".(int)$origin['id'];
$sql_origin_all .= " AND m.document_type = '".$db->escape($origin['type'])."'";
$sql_origin_all .= " ORDER BY m.line_order";
$res_origin_all = $db->query($sql_origin_all);
// Sammle alle Einträge gruppiert
$origin_entries = array();
while ($entry = $db->fetch_object($res_origin_all)) {
$origin_entries[] = $entry;
}
subtotaltitle_debug_log('✅ '.count($origin_entries).' Einträge aus Ursprung geladen');
// ============================================================
// SCHRITT 4: Hole ALLE Produktzeilen aus Zieldokument (Dolibarr-Tabelle)
// ============================================================
subtotaltitle_debug_log('📦 Hole alle Produkte aus Zieldokument...');
$sql_target_products = "SELECT rowid, fk_product, rang FROM ".MAIN_DB_PREFIX.$target_tables['lines_table'];
$sql_target_products .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id;
$sql_target_products .= " ORDER BY rang";
$res_target_products = $db->query($sql_target_products);
$target_products = array();
while ($row = $db->fetch_object($res_target_products)) {
$target_products[$row->rowid] = $row;
}
subtotaltitle_debug_log('✅ '.count($target_products).' Produkte aus Zieldokument geladen');
// Sammle fk_products die schon zugeordnet werden (aus Ursprung)
$assigned_fk_products = array();
$assigned_line_ids = array();
// ============================================================
// SCHRITT 5: Importiere Structure aus Ursprung mit richtiger Reihenfolge
// ============================================================
subtotaltitle_debug_log('🏗️ Importiere Struktur aus Ursprungsdokument...');
$line_order = 10;
$rang = 1;
$new_entries = array(); // Sammle alle neuen Einträge für spätere Rang-Zuweisung
foreach ($origin_entries as $entry) {
if ($entry->line_type === 'section') {
// Section importieren
$sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_insert .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title,";
$sql_insert .= " parent_section, show_subtotal, collapsed, line_order, in_facturedet, date_creation)";
$sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.",";
$sql_insert .= " '".$db->escape($target_type)."', 'section',";
$sql_insert .= " '".$db->escape($entry->title)."',";
$sql_insert .= " NULL,";
$sql_insert .= " ".(int)$entry->show_subtotal.",";
$sql_insert .= " ".(int)$entry->collapsed.",";
$sql_insert .= " ".(int)$line_order.",";
$sql_insert .= " 0, NOW())";
if (!$db->query($sql_insert)) {
throw new Exception('Fehler beim Erstellen der Section: '.$db->lasterror());
}
$new_section_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");
$section_mapping[$entry->rowid] = $new_section_id;
$imported_sections++;
$line_order += 10;
subtotaltitle_debug_log('✅ Section: "'.$entry->title.'" (#'.$entry->rowid.' → #'.$new_section_id.')');
} elseif ($entry->line_type === 'text') {
// Textline importieren
$new_parent = isset($section_mapping[$entry->parent_section])
? (int)$section_mapping[$entry->parent_section]
: 'NULL';
$sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_insert .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title,";
$sql_insert .= " parent_section, line_order, in_facturedet, date_creation)";
$sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.",";
$sql_insert .= " '".$db->escape($target_type)."', 'text',";
$sql_insert .= " '".$db->escape($entry->title)."',";
$sql_insert .= " ".$new_parent.",";
$sql_insert .= " ".(int)$line_order.",";
$sql_insert .= " 0, NOW())";
if (!$db->query($sql_insert)) {
throw new Exception('Fehler beim Erstellen der Textline: '.$db->lasterror());
}
$imported_textlines++;
$line_order += 10;
subtotaltitle_debug_log('✅ Textline: "'.$entry->title.'"');
} elseif ($entry->line_type === 'subtotal') {
// Subtotal importieren (nur wenn parent Section existiert)
if (!isset($section_mapping[$entry->parent_section])) {
continue;
}
$new_parent = (int)$section_mapping[$entry->parent_section];
$sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_insert .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title,";
$sql_insert .= " parent_section, line_order, in_facturedet, date_creation)";
$sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.",";
$sql_insert .= " '".$db->escape($target_type)."', 'subtotal',";
$sql_insert .= " '".$db->escape($entry->title)."',";
$sql_insert .= " ".$new_parent.",";
$sql_insert .= " ".(int)$line_order.",";
$sql_insert .= " 0, NOW())";
if (!$db->query($sql_insert)) {
throw new Exception('Fehler beim Erstellen des Subtotals: '.$db->lasterror());
}
$imported_subtotals++;
$line_order += 10;
subtotaltitle_debug_log('✅ Subtotal für Section #'.$new_parent);
} elseif ($entry->line_type === 'product' && !empty($entry->fk_product)) {
// Produkt - finde passende Zeile im Zieldokument
$target_line_id = null;
foreach ($target_products as $tp_id => $tp) {
if ($tp->fk_product == $entry->fk_product && !isset($assigned_line_ids[$tp_id])) {
$target_line_id = $tp_id;
$assigned_line_ids[$tp_id] = true;
$assigned_fk_products[$entry->fk_product] = true;
break;
}
}
if ($target_line_id) {
$new_parent = isset($section_mapping[$entry->parent_section])
? (int)$section_mapping[$entry->parent_section]
: 'NULL';
$sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_insert .= " (fk_facture, fk_propal, fk_commande, ".$target_tables['fk_line'].", document_type,";
$sql_insert .= " line_type, parent_section, line_order, in_facturedet, date_creation)";
$sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.",";
$sql_insert .= " ".(int)$target_line_id.", '".$db->escape($target_type)."',";
$sql_insert .= " 'product', ".$new_parent.", ".(int)$line_order.", 1, NOW())";
if (!$db->query($sql_insert)) {
throw new Exception('Fehler beim Erstellen des Produkts: '.$db->lasterror());
}
// Speichere für Rang-Update
$new_entries[] = array(
'type' => 'product',
'line_id' => $target_line_id,
'rang' => $rang
);
$product_assignments++;
$line_order += 10;
$rang++;
subtotaltitle_debug_log('✅ Produkt: fk_product='.$entry->fk_product.' → Section #'.$new_parent.' (Line #'.$target_line_id.')');
}
}
}
// ============================================================
// SCHRITT 6: Füge neue Produkte hinzu (nur im Zieldokument)
// ============================================================
subtotaltitle_debug_log(' Füge neue Produkte hinzu (nur im Zieldokument)...');
foreach ($target_products as $tp_id => $tp) {
if (isset($assigned_line_ids[$tp_id])) {
continue; // Schon zugeordnet
}
// Produkt ist NEU - füge am Ende hinzu (ohne Section)
$sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_insert .= " (fk_facture, fk_propal, fk_commande, ".$target_tables['fk_line'].", document_type,";
$sql_insert .= " line_type, parent_section, line_order, in_facturedet, date_creation)";
$sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.",";
$sql_insert .= " ".(int)$tp_id.", '".$db->escape($target_type)."',";
$sql_insert .= " 'product', NULL, ".(int)$line_order.", 1, NOW())";
if (!$db->query($sql_insert)) {
throw new Exception('Fehler beim Erstellen des neuen Produkts: '.$db->lasterror());
}
// Speichere für Rang-Update
$new_entries[] = array(
'type' => 'product',
'line_id' => $tp_id,
'rang' => $rang
);
$new_products++;
$line_order += 10;
$rang++;
subtotaltitle_debug_log(' Neues Produkt: Line #'.$tp_id.' (fk_product='.$tp->fk_product.')');
}
// ============================================================
// SCHRITT 7: Finale Neu-Nummerierung in beiden Tabellen
// ============================================================
subtotaltitle_debug_log('🔄 Finale Neu-Nummerierung in beiden Tabellen...');
// A) line_order in Manager-Tabelle (basierend auf tatsächlicher Reihenfolge)
$sql_reorder = "SELECT rowid, line_type, ".$target_tables['fk_line']." as fk_line FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_reorder .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id;
$sql_reorder .= " AND document_type = '".$db->escape($target_type)."'";
$sql_reorder .= " ORDER BY line_order";
$res_reorder = $db->query($sql_reorder);
$final_order = 10;
$final_rang = 1;
$product_rang_updates = array();
while ($row = $db->fetch_object($res_reorder)) {
// Update line_order
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager SET line_order = ".(int)$final_order;
$sql_upd .= " WHERE rowid = ".(int)$row->rowid;
$db->query($sql_upd);
// Sammle Rang für Produkte
if ($row->line_type === 'product' && $row->fk_line) {
$product_rang_updates[$row->fk_line] = $final_rang;
$final_rang++;
}
$final_order += 10;
}
subtotaltitle_debug_log('✅ line_order neu nummeriert');
// B) rang in Dolibarr-Tabelle aktualisieren
subtotaltitle_debug_log('🔄 Aktualisiere rang in Dolibarr-Tabelle ('.$target_tables['lines_table'].')...');
foreach ($product_rang_updates as $line_id => $new_rang) {
$sql_rang = "UPDATE ".MAIN_DB_PREFIX.$target_tables['lines_table'];
$sql_rang .= " SET rang = ".(int)$new_rang;
$sql_rang .= " WHERE rowid = ".(int)$line_id;
$db->query($sql_rang);
subtotaltitle_debug_log(' Line #'.$line_id.' → rang='.$new_rang);
}
subtotaltitle_debug_log('✅ rang in Dolibarr-Tabelle aktualisiert ('.count($product_rang_updates).' Zeilen)');
// Commit Transaktion
$db->commit();
subtotaltitle_debug_log('✅ Import komplett: '.$imported_sections.' Sections, '.$imported_textlines.' Textlines, '.$imported_subtotals.' Subtotals, '.$product_assignments.' zugeordnete Produkte, '.$new_products.' neue Produkte');
echo json_encode(array(
'success' => true,
'imported_sections' => $imported_sections,
'imported_textlines' => $imported_textlines,
'imported_subtotals' => $imported_subtotals,
'product_assignments' => $product_assignments,
'new_products' => $new_products,
'message' => sprintf('%d Sections, %d Textlines, %d Produkte zugeordnet, %d neue Produkte',
$imported_sections, $imported_textlines, $product_assignments, $new_products)
));
} catch (Exception $e) {
$db->rollback();
subtotaltitle_debug_log('❌ Import fehlgeschlagen: '.$e->getMessage());
echo json_encode(array('success' => false, 'error' => $e->getMessage()));
}
} else {
echo json_encode(array('success' => false, 'error' => 'Unknown action'));
}

View file

@ -2,53 +2,66 @@
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$line_ids_json = GETPOST('line_ids', 'alpha');
$facture_id = GETPOST('facture_id', 'int');
$document_id = GETPOST('facture_id', 'int'); // Kompatibilitaet: facture_id wird auch fuer andere Typen verwendet
$docType = GETPOST('document_type', 'alpha');
$line_ids = json_decode($line_ids_json, true);
if (!is_array($line_ids) || count($line_ids) == 0 || !$facture_id) {
if (!is_array($line_ids) || count($line_ids) == 0 || !$document_id) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
// Prüfe Rechnungsstatus
$facture = new Facture($db);
$facture->fetch($facture_id);
if ($facture->statut != Facture::STATUS_DRAFT) {
echo json_encode(['success' => false, 'error' => 'Rechnung ist nicht im Entwurf']);
// Hole die richtigen Tabellennamen fuer diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit;
}
subtotaltitle_debug_log('🗑️ Massenlöschung: ' . count($line_ids) . ' Zeilen');
// Pruefe Dokumentstatus
$object = DocumentTypeHelper::loadDocument($docType, $document_id, $db);
if (!$object) {
echo json_encode(['success' => false, 'error' => 'Dokument nicht gefunden']);
exit;
}
$isDraft = DocumentTypeHelper::isDraft($object, $docType);
if (!$isDraft) {
echo json_encode(['success' => false, 'error' => 'Dokument ist nicht im Entwurf']);
exit;
}
subtotaltitle_debug_log('Massenloeschung: ' . count($line_ids) . ' Zeilen, docType=' . $docType);
$db->begin();
$deleted = 0;
foreach ($line_ids as $line_id) {
$line_id = (int)$line_id;
// Aus facturedet löschen
$sql = "DELETE FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid = ".$line_id;
// Aus Detail-Tabelle loeschen
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".$line_id;
if ($db->query($sql)) {
$deleted++;
subtotaltitle_debug_log('✅ Zeile gelöscht: ' . $line_id);
subtotaltitle_debug_log('Zeile geloescht: ' . $line_id);
}
// Aus Manager-Tabelle löschen
$sql_manager = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE fk_facturedet = ".$line_id;
// Aus Manager-Tabelle loeschen
$sql_manager = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE ".$tables['fk_line']." = ".$line_id;
$db->query($sql_manager);
}
// Summen neu berechnen
$facture->update_price(1);
$object->update_price(1);
// line_order neu durchnummerieren
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
@ -60,21 +73,24 @@ while ($obj = $db->fetch_object($resql)) {
}
// rang synchronisieren
$sql = "SELECT fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " AND line_type = 'product'";
$sql = "SELECT ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND ".$tables['fk_line']." IS NOT NULL";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$rang = 1;
while ($obj = $db->fetch_object($resql)) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facturedet SET rang = ".$rang." WHERE rowid = ".(int)$obj->fk_facturedet;
$db->query($sql_upd);
$rang++;
if ($obj->detail_id) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table']." SET rang = ".$rang." WHERE rowid = ".(int)$obj->detail_id;
$db->query($sql_upd);
$rang++;
}
}
$db->commit();
subtotaltitle_debug_log('🗑️ Massenlöschung abgeschlossen: ' . $deleted . ' von ' . count($line_ids));
subtotaltitle_debug_log('Massenloeschung abgeschlossen: ' . $deleted . ' von ' . count($line_ids));
echo json_encode(['success' => true, 'deleted' => $deleted]);
echo json_encode(['success' => true, 'deleted' => $deleted]);

View file

@ -1,19 +1,41 @@
<?php
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$section_id = GETPOST('section_id', 'int');
$direction = GETPOST('direction', 'alpha');
$docType = GETPOST('document_type', 'alpha');
if (!$section_id || !$direction) {
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
exit;
}
// Wenn kein docType übergeben, versuche ihn aus der Section zu ermitteln
if (!$docType) {
$sql = "SELECT document_type FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE rowid = ".(int)$section_id;
$res = $db->query($sql);
if ($res && $obj = $db->fetch_object($res)) {
$docType = $obj->document_type;
}
}
if (!$docType) {
$docType = 'invoice'; // Fallback
}
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(array('success' => false, 'error' => 'Invalid document type'));
exit;
}
// Hole Section-Info
$sql = "SELECT fk_facture FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql = "SELECT ".$tables['fk_parent']." as doc_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE rowid = ".(int)$section_id;
$sql .= " AND line_type = 'section'";
$sql .= " AND document_type = '".$db->escape($docType)."'";
$resql = $db->query($sql);
if (!$resql || $db->num_rows($resql) == 0) {
@ -22,13 +44,14 @@ if (!$resql || $db->num_rows($resql) == 0) {
}
$section = $db->fetch_object($resql);
$facture_id = $section->fk_facture;
$doc_id = $section->doc_id;
$db->begin();
// 1. Hole alle Sections (sortiert)
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'section'";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
@ -42,18 +65,21 @@ while ($obj = $db->fetch_object($resql)) {
$current_index = array_search($section_id, $sections);
if ($current_index === false) {
$db->rollback();
echo json_encode(array('success' => false, 'error' => 'Section not in list'));
exit;
}
if ($direction == 'up') {
if ($current_index == 0) {
$db->rollback();
echo json_encode(array('success' => false, 'error' => 'Already at top'));
exit;
}
$swap_index = $current_index - 1;
} else {
if ($current_index == count($sections) - 1) {
$db->rollback();
echo json_encode(array('success' => false, 'error' => 'Already at bottom'));
exit;
}
@ -66,76 +92,104 @@ $sections[$current_index] = $sections[$swap_index];
$sections[$swap_index] = $temp;
// 3. Baue komplette neue Reihenfolge auf
// Strategie: Freie Zeilen behalten ihre Position relativ zu Sections
$new_order = 1;
$updates = array();
$processed = array();
// Freie Produkte zuerst
// Hole die aktuelle line_order der ersten Section (VOR dem Swap)
$sql = "SELECT MIN(line_order) as min_section_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'section'";
$resql = $db->query($sql);
$obj = $db->fetch_object($resql);
$first_section_order = $obj ? $obj->min_section_order : 9999;
// 1. FREIE ZEILEN VOR allen Sections (line_order < erste Section)
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " AND line_type = 'product'";
$sql .= " AND parent_section IS NULL";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND (parent_section IS NULL OR parent_section = 0)";
$sql .= " AND line_type != 'section'";
$sql .= " AND line_order < ".(int)$first_section_order;
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
$updates[$obj->rowid] = $new_order;
$processed[$obj->rowid] = true;
dol_syslog('[SubtotalTitle] move_section: Freie Zeile VOR Sections #'.$obj->rowid.' → line_order='.$new_order, LOG_INFO);
$new_order++;
}
// Freie Textzeilen
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " AND line_type = 'text'";
$sql .= " AND parent_section IS NULL";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
$updates[$obj->rowid] = $new_order;
$new_order++;
}
// Sections in neuer Reihenfolge
// 2. SECTIONS in neuer Reihenfolge (nach dem Swap)
foreach ($sections as $sec_id) {
// Section-Header
$updates[$sec_id] = $new_order;
$processed[$sec_id] = true;
dol_syslog('[SubtotalTitle] move_section: Section #'.$sec_id.' → line_order='.$new_order, LOG_INFO);
$new_order++;
// Produkte dieser Section
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'product'";
$sql .= " AND parent_section = ".(int)$sec_id;
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
$updates[$obj->rowid] = $new_order;
$processed[$obj->rowid] = true;
$new_order++;
}
// Textzeilen dieser Section
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'text'";
$sql .= " AND parent_section = ".(int)$sec_id;
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
$updates[$obj->rowid] = $new_order;
$processed[$obj->rowid] = true;
$new_order++;
}
// ========== SUBTOTAL DIESER SECTION ==========
// Subtotal dieser Section
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'subtotal'";
$sql .= " AND parent_section = ".(int)$sec_id;
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
$updates[$obj->rowid] = $new_order;
$processed[$obj->rowid] = true;
$new_order++;
}
}
// 3. FREIE ZEILEN NACH allen Sections (die noch nicht processed wurden)
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND (parent_section IS NULL OR parent_section = 0)";
$sql .= " AND line_type != 'section'";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
if (!isset($processed[$obj->rowid])) {
$updates[$obj->rowid] = $new_order;
$processed[$obj->rowid] = true;
dol_syslog('[SubtotalTitle] move_section: Freie Zeile NACH Sections #'.$obj->rowid.' → line_order='.$new_order, LOG_INFO);
$new_order++;
}
}
@ -148,22 +202,31 @@ foreach ($updates as $rowid => $order) {
$db->query($sql);
}
// 5. Sync rang
$sql = "SELECT fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " AND line_type = 'product'";
// 5. Sync rang in Detail-Tabelle
dol_syslog('[SubtotalTitle] move_section: Starte rang-Synchronisation für docType='.$docType.' doc_id='.$doc_id, LOG_INFO);
$sql = "SELECT rowid, line_type, ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND ".$tables['fk_line']." IS NOT NULL";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$rang = 1;
while ($obj = $db->fetch_object($resql)) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facturedet";
$sql_upd .= " SET rang = ".$rang;
$sql_upd .= " WHERE rowid = ".(int)$obj->fk_facturedet;
$db->query($sql_upd);
$rang++;
$detail_id = $obj->detail_id;
if ($detail_id) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_upd .= " SET rang = ".$rang;
$sql_upd .= " WHERE rowid = ".(int)$detail_id;
$db->query($sql_upd);
dol_syslog('[SubtotalTitle] Sync rang: '.$obj->line_type.' manager#'.$obj->rowid.' detail#'.$detail_id.' → rang='.$rang, LOG_INFO);
$rang++;
}
}
dol_syslog('[SubtotalTitle] move_section: rang-Synchronisation abgeschlossen, '.$rang.' Zeilen synchronisiert', LOG_INFO);
$db->commit();
echo json_encode(array('success' => true));
echo json_encode(array('success' => true));

View file

@ -2,24 +2,112 @@
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$product_id = GETPOST('product_id', 'int');
$docType = GETPOST('document_type', 'alpha');
subtotaltitle_debug_log('🔓 remove_from_section: product=' . $product_id);
subtotaltitle_debug_log('🔓 remove_from_section: product=' . $product_id . ', docType=' . $docType);
if (!$product_id) {
echo json_encode(['success' => false, 'error' => 'Missing product_id']);
if (!$product_id || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
// Hole die richtigen Tabellennamen für diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit;
}
$db->begin();
// 1. Hole parent_section und document_id BEVOR wir entfernen
$sql = "SELECT parent_section, ".$tables['fk_parent']." as doc_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_line']." = ".(int)$product_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$resql = $db->query($sql);
if (!$resql || $db->num_rows($resql) == 0) {
$db->rollback();
echo json_encode(['success' => false, 'error' => 'Product not found in manager table']);
exit;
}
$obj = $db->fetch_object($resql);
$old_section_id = $obj->parent_section;
$document_id = $obj->doc_id;
// 2. Entferne aus Section
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " SET parent_section = NULL";
$sql .= " WHERE fk_facturedet = ".(int)$product_id;
$sql .= " WHERE ".$tables['fk_line']." = ".(int)$product_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$db->query($sql);
$result = $db->query($sql);
subtotaltitle_debug_log('✅ Produkt #' . $product_id . ' aus Section #'.$old_section_id.' entfernt');
if ($result) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => $db->lasterror()]);
}
// 3. Wenn Product aus einer Section entfernt wurde, Subtotal neu berechnen
if ($old_section_id > 0) {
// Prüfe ob Section Subtotal anzeigen soll
$sql = "SELECT show_subtotal, title FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE rowid = ".(int)$old_section_id;
$sql .= " AND line_type = 'section'";
$resql = $db->query($sql);
if ($resql && $obj = $db->fetch_object($resql)) {
if ($obj->show_subtotal) {
// Berechne neue Summe
$sql_sum = "SELECT SUM(d.total_ht) as total";
$sql_sum .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql_sum .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
$sql_sum .= " WHERE m.parent_section = ".(int)$old_section_id;
$sql_sum .= " AND m.document_type = '".$db->escape($docType)."'";
$sql_sum .= " AND m.line_type = 'product'";
$res_sum = $db->query($sql_sum);
$obj_sum = $db->fetch_object($res_sum);
$new_total = $obj_sum->total ? (float)$obj_sum->total : 0;
// Update Subtotal in Detail-Tabelle
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table']." d";
$sql_upd .= " INNER JOIN ".MAIN_DB_PREFIX."facture_lines_manager m ON m.".$tables['fk_line']." = d.rowid";
$sql_upd .= " SET d.subprice = ".(float)$new_total.", d.total_ht = ".(float)$new_total.", d.total_ttc = ".(float)$new_total;
$sql_upd .= " WHERE m.parent_section = ".(int)$old_section_id;
$sql_upd .= " AND m.document_type = '".$db->escape($docType)."'";
$sql_upd .= " AND m.line_type = 'subtotal'";
$db->query($sql_upd);
subtotaltitle_debug_log(' Subtotal für Section #'.$old_section_id.' aktualisiert: '.$new_total);
}
}
}
// 4. Sync rang in Detail-Tabelle
subtotaltitle_debug_log(' Starte rang-Synchronisation für docType='.$docType.' doc_id='.$document_id);
$sql = "SELECT rowid, line_type, ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND ".$tables['fk_line']." IS NOT NULL";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$rang = 1;
while ($obj = $db->fetch_object($resql)) {
$detail_id = $obj->detail_id;
if ($detail_id) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_upd .= " SET rang = ".$rang;
$sql_upd .= " WHERE rowid = ".(int)$detail_id;
$db->query($sql_upd);
subtotaltitle_debug_log(' Sync rang: '.$obj->line_type.' manager#'.$obj->rowid.' detail#'.$detail_id.' → rang='.$rang);
$rang++;
}
}
subtotaltitle_debug_log(' rang-Synchronisation abgeschlossen, '.$rang.' Zeilen synchronisiert');
$db->commit();
echo json_encode(['success' => true]);

View file

@ -2,17 +2,26 @@
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$facture_id = GETPOST('facture_id', 'int');
$new_order_json = GETPOST('new_order', 'none');
$docType = GETPOST('document_type', 'alpha');
subtotaltitle_debug_log('🔄 reorder_all: facture=' . $facture_id);
if (!$facture_id || !$new_order_json) {
if (!$facture_id || !$new_order_json || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
// Hole die richtigen Tabellennamen für diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit;
}
$new_order = json_decode($new_order_json, true);
if (!$new_order) {
@ -35,7 +44,7 @@ foreach ($new_order as $item) {
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " SET line_order = ".(int)$item['order'];
$sql .= ", parent_section = ".($item['parent_section'] ? (int)$item['parent_section'] : "NULL");
$sql .= " WHERE fk_facturedet = ".(int)$item['id'];
$sql .= " WHERE ".$tables['fk_line']." = ".(int)$item['id'];
$db->query($sql);
subtotaltitle_debug_log(' Produkt #'.$item['id'].' → order='.$item['order'].', section='.($item['parent_section'] ?: 'FREI'));
@ -53,29 +62,70 @@ foreach ($new_order as $item) {
// ========== SUBTOTALS NEU POSITIONIEREN ==========
subtotaltitle_debug_log('🔢 Repositioniere Subtotals...');
// Hole alle Subtotals für dieses Dokument
$sql = "SELECT rowid, parent_section FROM ".MAIN_DB_PREFIX."facture_lines_manager
WHERE fk_facture = ".(int)$facture_id."
WHERE ".$tables['fk_parent']." = ".(int)$facture_id."
AND document_type = '".$db->escape($docType)."'
AND line_type = 'subtotal'";
$resql = $db->query($sql);
$subtotals_to_update = array();
while ($subtotal = $db->fetch_object($resql)) {
$subtotals_to_update[] = $subtotal;
}
foreach ($subtotals_to_update as $subtotal) {
// Finde höchste line_order der Produkte dieser Section
$sql_max = "SELECT MAX(line_order) as max_order
FROM ".MAIN_DB_PREFIX."facture_lines_manager
WHERE parent_section = ".(int)$subtotal->parent_section."
AND line_type = 'product'";
AND line_type = 'product'
AND document_type = '".$db->escape($docType)."'";
$res_max = $db->query($sql_max);
$obj_max = $db->fetch_object($res_max);
if ($obj_max && $obj_max->max_order) {
// Subtotal bekommt hohe Nummer (wird gleich normalisiert)
$temp_order = (int)$obj_max->max_order * 100 + 50;
// Subtotal kommt direkt nach dem letzten Produkt: max_order + 0.5
$temp_order = (float)$obj_max->max_order + 0.5;
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager
SET line_order = ".$temp_order."
WHERE rowid = ".(int)$subtotal->rowid;
$db->query($sql_upd);
subtotaltitle_debug_log(' Subtotal #'.$subtotal->rowid.' → temp_order='.$temp_order.' (nach Section '.$subtotal->parent_section.')');
subtotaltitle_debug_log(' Subtotal #'.$subtotal->rowid.' (Section '.$subtotal->parent_section.') → temp_order='.$temp_order);
} else {
subtotaltitle_debug_log(' ⚠️ Subtotal #'.$subtotal->rowid.' hat keine Produkte in Section '.$subtotal->parent_section);
}
}
// ========== SECTIONS VOR IHREN PRODUKTEN SICHERSTELLEN ==========
subtotaltitle_debug_log('🔧 Stelle sicher dass Sections vor ihren Produkten stehen...');
// Für jede Section: Wenn ein Produkt dieser Section VOR der Section steht,
// verschiebe die Section vor das erste Produkt
$sql_sections = "SELECT rowid, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager
WHERE ".$tables['fk_parent']." = ".(int)$facture_id."
AND document_type = '".$db->escape($docType)."'
AND line_type = 'section'";
$res_sections = $db->query($sql_sections);
while ($section = $db->fetch_object($res_sections)) {
// Finde das Produkt dieser Section mit der kleinsten line_order
$sql_first_prod = "SELECT MIN(line_order) as min_order FROM ".MAIN_DB_PREFIX."facture_lines_manager
WHERE parent_section = ".(int)$section->rowid."
AND document_type = '".$db->escape($docType)."'
AND line_type = 'product'";
$res_first = $db->query($sql_first_prod);
$first_prod = $db->fetch_object($res_first);
if ($first_prod && $first_prod->min_order && $first_prod->min_order < $section->line_order) {
// Section steht NACH ihrem ersten Produkt - verschiebe sie davor
$new_section_order = (float)$first_prod->min_order - 0.5;
$sql_move = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager
SET line_order = ".$new_section_order."
WHERE rowid = ".(int)$section->rowid;
$db->query($sql_move);
subtotaltitle_debug_log(' Section #'.$section->rowid.' verschoben: '.$section->line_order.' → '.$new_section_order.' (vor Produkt mit order='.$first_prod->min_order.')');
}
}
@ -83,7 +133,8 @@ while ($subtotal = $db->fetch_object($resql)) {
subtotaltitle_debug_log('🔢 Normalisiere line_order...');
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager
WHERE fk_facture = ".(int)$facture_id."
WHERE ".$tables['fk_parent']." = ".(int)$facture_id."
AND document_type = '".$db->escape($docType)."'
ORDER BY line_order";
$resql = $db->query($sql);
@ -96,19 +147,23 @@ while ($obj = $db->fetch_object($resql)) {
$new_order_num++;
}
// ========== SYNC FACTUREDET.RANG ==========
$sql = "SELECT fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " AND line_type = 'product'";
// ========== SYNC RANG IN DETAIL-TABELLE ==========
// Synchronisiere ALLE Zeilen die in der Detail-Tabelle sind (nicht nur Produkte!)
$sql = "SELECT ".$tables['fk_line'].", line_type FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND ".$tables['fk_line']." IS NOT NULL"; // Alle die in Detail-Tabelle sind
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$rang = 1;
while ($obj = $db->fetch_object($resql)) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facturedet";
$fk_line_value = $obj->{$tables['fk_line']};
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_upd .= " SET rang = ".$rang;
$sql_upd .= " WHERE rowid = ".(int)$obj->fk_facturedet;
$sql_upd .= " WHERE rowid = ".(int)$fk_line_value;
$db->query($sql_upd);
subtotaltitle_debug_log(' Sync rang: '.$obj->line_type.' #'.$fk_line_value.' → rang='.$rang);
$rang++;
}

View file

@ -0,0 +1,73 @@
<?php
/**
* Repariert fehlende Subtotal-Zeilen für existierende Sections
*/
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$facture_id = GETPOST('facture_id', 'int');
$docType = GETPOST('document_type', 'alpha');
if (!$facture_id || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit;
}
$repaired = 0;
// Finde alle Sections, die keine Subtotal-Zeile haben
$sql = "SELECT s.rowid, s.title";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager s";
$sql .= " WHERE s.".$tables['fk_parent']." = ".(int)$facture_id;
$sql .= " AND s.document_type = '".$db->escape($docType)."'";
$sql .= " AND s.line_type = 'section'";
$sql .= " AND NOT EXISTS (";
$sql .= " SELECT 1 FROM ".MAIN_DB_PREFIX."facture_lines_manager sub";
$sql .= " WHERE sub.parent_section = s.rowid";
$sql .= " AND sub.line_type = 'subtotal'";
$sql .= " AND sub.document_type = '".$db->escape($docType)."'";
$sql .= " )";
$resql = $db->query($sql);
while ($section = $db->fetch_object($resql)) {
subtotaltitle_debug_log('🔧 Repariere Section #'.$section->rowid.' ('.$section->title.') - erstelle Subtotal');
// Hole nächste line_order
$sql_max = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_max .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_max .= " AND document_type = '".$db->escape($docType)."'";
$res_max = $db->query($sql_max);
$obj_max = $db->fetch_object($res_max);
$next_order = ($obj_max && $obj_max->max_order) ? $obj_max->max_order + 1 : 9999;
$fk_facture = ($docType === 'invoice') ? (int)$facture_id : 'NULL';
$fk_propal = ($docType === 'propal') ? (int)$facture_id : 'NULL';
$fk_commande = ($docType === 'order') ? (int)$facture_id : 'NULL';
$sql_ins = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_ins .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title, parent_section, line_order, date_creation)";
$sql_ins .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'subtotal', 'Zwischensumme', ".(int)$section->rowid.", ".$next_order.", NOW())";
if ($db->query($sql_ins)) {
$repaired++;
subtotaltitle_debug_log('✅ Subtotal-Zeile erstellt für Section #'.$section->rowid);
} else {
subtotaltitle_debug_log('❌ Fehler beim Erstellen der Subtotal-Zeile für Section #'.$section->rowid.': '.$db->lasterror());
}
}
echo json_encode([
'success' => true,
'repaired' => $repaired,
'message' => $repaired.' Subtotal-Zeilen erstellt'
]);

265
ajax/sync_to_facturedet.php Normal file → Executable file
View file

@ -13,6 +13,7 @@ if (!$res) die("Include of main fails");
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
dol_include_once('/subtotaltitle/lib/subtotaltitle.lib.php');
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
header('Content-Type: application/json');
@ -20,14 +21,22 @@ $action = GETPOST('action', 'alpha');
$line_id = GETPOST('line_id', 'int');
$line_type = GETPOST('line_type', 'alpha');
$facture_id = GETPOST('facture_id', 'int');
$docType = GETPOST('document_type', 'alpha');
subtotaltitle_debug_log('🔄 sync_to_facturedet: action='.$action.', line_id='.$line_id.', type='.$line_type);
subtotaltitle_debug_log('🔄 sync_to_facturedet: action='.$action.', line_id='.$line_id.', type='.$line_type.', docType='.$docType);
if (!$line_id || !$action) {
if (!$line_id || !$action || !$docType) {
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
exit;
}
// Hole die richtigen Tabellennamen für diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(array('success' => false, 'error' => 'Invalid document type'));
exit;
}
// Special codes für unsere Zeilentypen
$special_codes = array(
'section' => 100,
@ -44,22 +53,55 @@ if ($action == 'add') {
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."facture_lines_manager s ON s.rowid = m.parent_section";
$sql .= " WHERE m.rowid = ".(int)$line_id;
$resql = $db->query($sql);
if (!$resql || $db->num_rows($resql) == 0) {
echo json_encode(array('success' => false, 'error' => 'Line not found'));
exit;
}
$line = $db->fetch_object($resql);
$facture_id = $line->fk_facture;
$document_id = $line->{$tables['fk_parent']};
$line_type = $line->line_type;
// Prüfe ob schon in facturedet (für nicht-Produkte)
if ($line->fk_facturedet > 0 && $line_type != 'product') {
echo json_encode(array('success' => false, 'error' => 'Already in facturedet'));
$fk_line_field = $tables['fk_line'];
// Prüfe ob schon in detail-Tabelle (für nicht-Produkte)
if ($line->$fk_line_field > 0 && $line_type != 'product') {
echo json_encode(array('success' => false, 'error' => 'Already in detail table'));
exit;
}
// AUTOMATISCHE REPARATUR: Wenn Section keine Subtotal-Zeile hat, erstelle sie
if ($line_type == 'section') {
$sql_check = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_check .= " WHERE parent_section = ".(int)$line_id;
$sql_check .= " AND line_type = 'subtotal'";
$sql_check .= " AND document_type = '".$db->escape($docType)."'";
$res_check = $db->query($sql_check);
if ($db->num_rows($res_check) == 0) {
// Keine Subtotal-Zeile vorhanden - automatisch erstellen
subtotaltitle_debug_log('⚠️ Section #'.$line_id.' hat keine Subtotal-Zeile - erstelle automatisch');
$sql_max = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_max .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_max .= " AND document_type = '".$db->escape($docType)."'";
$res_max = $db->query($sql_max);
$obj_max = $db->fetch_object($res_max);
$next_order = ($obj_max && $obj_max->max_order) ? $obj_max->max_order + 1 : 9999;
$fk_facture = ($docType === 'invoice') ? (int)$document_id : 'NULL';
$fk_propal = ($docType === 'propal') ? (int)$document_id : 'NULL';
$fk_commande = ($docType === 'order') ? (int)$document_id : 'NULL';
$sql_subtotal = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_subtotal .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title, parent_section, line_order, date_creation)";
$sql_subtotal .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'subtotal', 'Zwischensumme', ".(int)$line_id.", ".$next_order.", NOW())";
$db->query($sql_subtotal);
subtotaltitle_debug_log('✅ Subtotal-Zeile automatisch erstellt für Section #'.$line_id);
}
}
// Bestimme special_code
$special_code = isset($special_codes[$line_type]) ? $special_codes[$line_type] : 0;
@ -83,43 +125,51 @@ if ($action == 'add') {
// Berechne Summe der Section
$sql_sum = "SELECT SUM(d.total_ht) as total";
$sql_sum .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql_sum .= " INNER JOIN ".MAIN_DB_PREFIX."facturedet d ON d.rowid = m.fk_facturedet";
$sql_sum .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
$sql_sum .= " WHERE m.parent_section = ".(int)$line->parent_section;
$sql_sum .= " AND m.document_type = '".$db->escape($docType)."'";
$sql_sum .= " AND m.line_type = 'product'";
$res_sum = $db->query($sql_sum);
$obj_sum = $db->fetch_object($res_sum);
$total_ht = $obj_sum->total ? $obj_sum->total : 0;
$description = 'Zwischensumme: '.$line->section_title;
$qty = 1;
break;
}
// Bestimme rang (Position)
// Bestimme rang (Position) - UNTERSCHIEDLICH für Sections vs andere Zeilen
$new_rang = 1;
// EINHEITLICHE BERECHNUNG für ALLE Zeilentypen basierend auf line_order
// Finde den höchsten rang aller Zeilen, die VOR dieser Zeile (nach line_order) liegen
$sql_rang = "SELECT MAX(d.rang) as max_rang";
$sql_rang .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql_rang .= " INNER JOIN ".MAIN_DB_PREFIX."facturedet d ON d.rowid = m.fk_facturedet";
$sql_rang .= " WHERE m.fk_facture = ".(int)$facture_id;
$sql_rang .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
$sql_rang .= " WHERE m.".$tables['fk_parent']." = ".(int)$document_id;
$sql_rang .= " AND m.document_type = '".$db->escape($docType)."'";
$sql_rang .= " AND m.line_order < ".(int)$line->line_order;
$res_rang = $db->query($sql_rang);
$obj_rang = $db->fetch_object($res_rang);
$new_rang = ($obj_rang && $obj_rang->max_rang) ? $obj_rang->max_rang + 1 : 1;
subtotaltitle_debug_log('📝 Berechne rang: line_type='.$line_type.', new_rang='.$new_rang);
// Verschiebe alle nachfolgenden Zeilen
$sql_shift = "UPDATE ".MAIN_DB_PREFIX."facturedet";
$sql_shift = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_shift .= " SET rang = rang + 1";
$sql_shift .= " WHERE fk_facture = ".(int)$facture_id;
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_shift .= " AND rang >= ".(int)$new_rang;
$db->query($sql_shift);
// Füge neue Zeile in facturedet ein
// Füge neue Zeile in Detail-Tabelle ein
subtotaltitle_debug_log('📝 INSERT: line_type='.$line_type.', special_code='.$special_code);
$sql_ins = "INSERT INTO ".MAIN_DB_PREFIX."facturedet";
$sql_ins .= " (fk_facture, description, qty, subprice, total_ht, total_tva, total_ttc,";
$sql_ins = "INSERT INTO ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_ins .= " (".$tables['fk_parent'].", description, qty, subprice, total_ht, total_tva, total_ttc,";
$sql_ins .= " tva_tx, product_type, special_code, rang, info_bits)";
$sql_ins .= " VALUES (";
$sql_ins .= (int)$facture_id.", ";
$sql_ins .= (int)$document_id.", ";
$sql_ins .= "'".$db->escape($description)."', ";
$sql_ins .= (float)$qty.", ";
$sql_ins .= ($line_type == 'subtotal') ? (float)$total_ht.", " : "0, ";
@ -133,134 +183,209 @@ if ($action == 'add') {
$sql_ins .= "0)";
subtotaltitle_debug_log('📝 SQL: '.$sql_ins);
if (!$db->query($sql_ins)) {
echo json_encode(array('success' => false, 'error' => $db->lasterror()));
exit;
}
$new_facturedet_id = $db->last_insert_id(MAIN_DB_PREFIX."facturedet");
$new_detail_id = $db->last_insert_id(MAIN_DB_PREFIX.$tables['lines_table']);
// Update unsere Manager-Tabelle
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_upd .= " SET fk_facturedet = ".(int)$new_facturedet_id;
$sql_upd .= " SET ".$tables['fk_line']." = ".(int)$new_detail_id;
$sql_upd .= ", in_facturedet = 1";
$sql_upd .= " WHERE rowid = ".(int)$line_id;
$db->query($sql_upd);
subtotaltitle_debug_log('✅ Zeile #'.$line_id.' zu facturedet hinzugefügt als #'.$new_facturedet_id);
subtotaltitle_debug_log('✅ Zeile #'.$line_id.' zu '.$tables['lines_table'].' hinzugefügt als #'.$new_detail_id);
echo json_encode(array(
'success' => true,
'facturedet_id' => $new_facturedet_id,
'success' => true,
'detail_id' => $new_detail_id,
'rang' => $new_rang
));
} elseif ($action == 'remove') {
// ========== AUS RECHNUNG ENTFERNEN ==========
// ========== AUS DETAIL-TABELLE ENTFERNEN ==========
// Hole Daten
$sql = "SELECT fk_facturedet, fk_facture, line_type FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql = "SELECT ".$tables['fk_line']." as detail_id, ".$tables['fk_parent']." as parent_id, line_type FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE rowid = ".(int)$line_id;
$resql = $db->query($sql);
if (!$resql || $db->num_rows($resql) == 0) {
echo json_encode(array('success' => false, 'error' => 'Line not found'));
exit;
}
$line = $db->fetch_object($resql);
// Produkte dürfen nicht entfernt werden
if ($line->line_type == 'product') {
echo json_encode(array('success' => false, 'error' => 'Cannot remove products'));
exit;
}
if (!$line->fk_facturedet) {
echo json_encode(array('success' => false, 'error' => 'Not in facturedet'));
if (!$line->detail_id) {
echo json_encode(array('success' => false, 'error' => 'Not in detail table'));
exit;
}
// Hole rang bevor wir löschen
$sql_rang = "SELECT rang FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid = ".(int)$line->fk_facturedet;
$sql_rang = "SELECT rang FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".(int)$line->detail_id;
$res_rang = $db->query($sql_rang);
$obj_rang = $db->fetch_object($res_rang);
$old_rang = $obj_rang ? $obj_rang->rang : 0;
// Lösche aus facturedet
$sql_del = "DELETE FROM ".MAIN_DB_PREFIX."facturedet";
$sql_del .= " WHERE rowid = ".(int)$line->fk_facturedet;
// Lösche aus Detail-Tabelle
$sql_del = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_del .= " WHERE rowid = ".(int)$line->detail_id;
if (!$db->query($sql_del)) {
echo json_encode(array('success' => false, 'error' => $db->lasterror()));
exit;
}
// Schließe Lücke in rang
if ($old_rang > 0) {
$sql_shift = "UPDATE ".MAIN_DB_PREFIX."facturedet";
$sql_shift = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_shift .= " SET rang = rang - 1";
$sql_shift .= " WHERE fk_facture = ".(int)$line->fk_facture;
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$line->parent_id;
$sql_shift .= " AND rang > ".(int)$old_rang;
$db->query($sql_shift);
}
// Update unsere Manager-Tabelle
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_upd .= " SET fk_facturedet = NULL";
$sql_upd .= " SET ".$tables['fk_line']." = NULL";
$sql_upd .= ", in_facturedet = 0";
$sql_upd .= " WHERE rowid = ".(int)$line_id;
$db->query($sql_upd);
subtotaltitle_debug_log('✅ Zeile #'.$line_id.' aus facturedet entfernt');
subtotaltitle_debug_log('✅ Zeile #'.$line_id.' aus '.$tables['lines_table'].' entfernt');
echo json_encode(array('success' => true));
} elseif ($action == 'update_subtotal') {
// ========== SUBTOTAL-BETRAG AKTUALISIEREN ==========
$sql = "SELECT m.fk_facturedet, m.parent_section, s.title as section_title";
$sql = "SELECT m.".$tables['fk_line']." as detail_id, m.parent_section, s.title as section_title";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."facture_lines_manager s ON s.rowid = m.parent_section";
$sql .= " WHERE m.rowid = ".(int)$line_id;
$sql .= " AND m.line_type = 'subtotal'";
$resql = $db->query($sql);
if (!$resql || $db->num_rows($resql) == 0) {
echo json_encode(array('success' => false, 'error' => 'Subtotal not found'));
exit;
}
$line = $db->fetch_object($resql);
if (!$line->fk_facturedet) {
echo json_encode(array('success' => false, 'error' => 'Not in facturedet'));
if (!$line->detail_id) {
echo json_encode(array('success' => false, 'error' => 'Not in detail table'));
exit;
}
// Berechne neue Summe
$sql_sum = "SELECT SUM(d.total_ht) as total";
$sql_sum .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql_sum .= " INNER JOIN ".MAIN_DB_PREFIX."facturedet d ON d.rowid = m.fk_facturedet";
$sql_sum .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
$sql_sum .= " WHERE m.parent_section = ".(int)$line->parent_section;
$sql_sum .= " AND m.document_type = '".$db->escape($docType)."'";
$sql_sum .= " AND m.line_type = 'product'";
$res_sum = $db->query($sql_sum);
$obj_sum = $db->fetch_object($res_sum);
$total_ht = $obj_sum->total ? $obj_sum->total : 0;
// Update facturedet
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facturedet";
// Update Detail-Tabelle
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_upd .= " SET subprice = ".(float)$total_ht;
$sql_upd .= ", total_ht = ".(float)$total_ht;
$sql_upd .= ", total_ttc = ".(float)$total_ht;
$sql_upd .= " WHERE rowid = ".(int)$line->fk_facturedet;
$sql_upd .= " WHERE rowid = ".(int)$line->detail_id;
$db->query($sql_upd);
subtotaltitle_debug_log('✅ Subtotal #'.$line_id.' aktualisiert: '.$total_ht);
echo json_encode(array('success' => true, 'total_ht' => $total_ht));
} elseif ($action == 'remove_all') {
// ========== ALLE SPEZIALZEILEN UND VERWAISTE EINTRÄGE ENTFERNEN ==========
// document_id wird benötigt
$document_id = GETPOST('document_id', 'int');
if (!$document_id) {
echo json_encode(array('success' => false, 'error' => 'Missing document_id'));
exit;
}
$removed_count = 0;
$orphan_count = 0;
// 1. Entferne ALLE Einträge mit special_code 100, 101, 102 aus der Detail-Tabelle
// (unabhängig davon ob sie noch in der Manager-Tabelle existieren)
$sql_delete_all = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_delete_all .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_delete_all .= " AND special_code IN (100, 101, 102)";
subtotaltitle_debug_log('🗑️ Remove ALL special lines: '.$sql_delete_all);
if ($db->query($sql_delete_all)) {
$removed_count = $db->affected_rows($db->query("SELECT ROW_COUNT()"));
// Fallback: Zähle manuell wenn affected_rows nicht funktioniert
if ($removed_count === 0) {
// Zähle vorher
$sql_count = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_count .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_count .= " AND special_code IN (100, 101, 102)";
// Da wir schon gelöscht haben, ist es jetzt 0
$removed_count = -1; // Unbekannt, aber erfolgreich
}
}
// 2. Setze in_facturedet und fk_*det auf NULL für alle Manager-Einträge dieses Dokuments
$sql_reset = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_reset .= " SET ".$tables['fk_line']." = NULL, in_facturedet = 0";
$sql_reset .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_reset .= " AND document_type = '".$db->escape($docType)."'";
$sql_reset .= " AND line_type IN ('section', 'text', 'subtotal')";
subtotaltitle_debug_log('🔄 Reset manager entries: '.$sql_reset);
$db->query($sql_reset);
// 3. Normalisiere die rang-Werte (schließe Lücken)
$sql_reorder = "SET @r = 0; UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_reorder .= " SET rang = (@r := @r + 1)";
$sql_reorder .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_reorder .= " ORDER BY rang";
// MySQL erlaubt kein SET in einer Anweisung mit UPDATE, also manuell:
$sql_get_lines = "SELECT rowid FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_get_lines .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_get_lines .= " ORDER BY rang";
$res_lines = $db->query($sql_get_lines);
$new_rang = 1;
while ($obj = $db->fetch_object($res_lines)) {
$sql_upd_rang = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_upd_rang .= " SET rang = ".(int)$new_rang;
$sql_upd_rang .= " WHERE rowid = ".(int)$obj->rowid;
$db->query($sql_upd_rang);
$new_rang++;
}
subtotaltitle_debug_log('✅ Remove ALL completed: removed='.$removed_count);
echo json_encode(array(
'success' => true,
'removed' => $removed_count,
'orphans_cleaned' => $orphan_count,
'message' => 'Alle Spezialzeilen wurden entfernt'
));
} else {
echo json_encode(array('success' => false, 'error' => 'Unknown action'));
}

View file

@ -2,23 +2,32 @@
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
dol_include_once('/subtotaltitle/lib/subtotaltitle.lib.php');
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$section_id = GETPOST('section_id', 'int');
$show = GETPOST('show', 'int');
$docType = GETPOST('document_type', 'alpha');
subtotaltitle_debug_log('🔢 toggle_subtotal: section=' . $section_id . ', show=' . $show);
subtotaltitle_debug_log('🔢 toggle_subtotal: section=' . $section_id . ', show=' . $show . ', docType=' . $docType);
if (!$section_id) {
echo json_encode(['success' => false, 'error' => 'Missing section_id']);
if (!$section_id || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
// Hole die richtigen Tabellennamen für diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit;
}
// Hole Section-Info
$sql = "SELECT fk_facture, title FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql = "SELECT fk_facture, fk_propal, fk_commande, title, document_type FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE rowid = ".(int)$section_id;
$resql = $db->query($sql);
$section = $db->fetch_object($resql);
$facture_id = $section->fk_facture;
$facture_id = $section->{$tables['fk_parent']};
$db->begin();
@ -39,8 +48,9 @@ if ($show) {
// Berechne Zwischensumme
$sql_sum = "SELECT SUM(d.total_ht) as total";
$sql_sum .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql_sum .= " INNER JOIN ".MAIN_DB_PREFIX."facturedet d ON d.rowid = m.fk_facturedet";
$sql_sum .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
$sql_sum .= " WHERE m.parent_section = ".(int)$section_id;
$sql_sum .= " AND m.document_type = '".$db->escape($docType)."'";
$sql_sum .= " AND m.line_type = 'product'";
$res_sum = $db->query($sql_sum);
$obj_sum = $db->fetch_object($res_sum);
@ -53,46 +63,64 @@ if ($show) {
$res_last = $db->query($sql_last);
$obj_last = $db->fetch_object($res_last);
$last_order = $obj_last->max_order ? $obj_last->max_order : 0;
// Neue line_order = nach letztem Produkt
// Wenn keine Produkte in der Section, nimm die line_order der Section selbst
if ($last_order == 0) {
$sql_sec = "SELECT line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_sec .= " WHERE rowid = ".(int)$section_id;
$res_sec = $db->query($sql_sec);
$obj_sec = $db->fetch_object($res_sec);
$last_order = $obj_sec->line_order ? $obj_sec->line_order : 0;
}
// Neue line_order = nach letztem Produkt (oder nach der Section selbst wenn leer)
$new_order = $last_order + 1;
// Alle nachfolgenden Zeilen +1
$sql_shift = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_shift .= " SET line_order = line_order + 1";
$sql_shift .= " WHERE fk_facture = ".(int)$facture_id;
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_shift .= " AND document_type = '".$db->escape($docType)."'";
$sql_shift .= " AND line_order >= ".$new_order;
$db->query($sql_shift);
// Bestimme FK-Felder
$fk_facture = ($docType === 'invoice') ? (int)$facture_id : 'NULL';
$fk_propal = ($docType === 'propal') ? (int)$facture_id : 'NULL';
$fk_commande = ($docType === 'order') ? (int)$facture_id : 'NULL';
// Subtotal-Zeile in Manager-Tabelle einfügen
$sql_ins = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_ins .= " (fk_facture, line_type, parent_section, title, line_order, date_creation)";
$sql_ins .= " VALUES (".(int)$facture_id.", 'subtotal', ".(int)$section_id.", 'Zwischensumme: ".addslashes($section->title)."', ".$new_order.", NOW())";
$sql_ins .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, parent_section, title, line_order, date_creation)";
$sql_ins .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'subtotal', ".(int)$section_id.", 'Zwischensumme: ".addslashes($section->title)."', ".$new_order.", NOW())";
$db->query($sql_ins);
$subtotal_manager_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");
// Subtotal-Zeile auch direkt in facturedet einfügen
// Bestimme rang (nach letztem Produkt der Section)
// Subtotal-Zeile auch direkt in Detail-Tabelle einfügen
// Bestimme rang basierend auf line_order (EINHEITLICH für alle Zeilentypen)
// Finde den höchsten rang aller Zeilen, die VOR dieser neuen Subtotal-Zeile liegen
$sql_rang = "SELECT MAX(d.rang) as max_rang";
$sql_rang .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql_rang .= " INNER JOIN ".MAIN_DB_PREFIX."facturedet d ON d.rowid = m.fk_facturedet";
$sql_rang .= " WHERE m.parent_section = ".(int)$section_id;
$sql_rang .= " AND m.line_type = 'product'";
$sql_rang .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
$sql_rang .= " WHERE m.".$tables['fk_parent']." = ".(int)$facture_id;
$sql_rang .= " AND m.document_type = '".$db->escape($docType)."'";
$sql_rang .= " AND m.line_order < ".(int)$new_order;
$res_rang = $db->query($sql_rang);
$obj_rang = $db->fetch_object($res_rang);
$new_rang = ($obj_rang && $obj_rang->max_rang) ? $obj_rang->max_rang + 1 : 1;
// Verschiebe nachfolgende Zeilen
$sql_shift_fd = "UPDATE ".MAIN_DB_PREFIX."facturedet";
$sql_shift_fd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_shift_fd .= " SET rang = rang + 1";
$sql_shift_fd .= " WHERE fk_facture = ".(int)$facture_id;
$sql_shift_fd .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_shift_fd .= " AND rang >= ".(int)$new_rang;
$db->query($sql_shift_fd);
// Füge Subtotal in facturedet ein (special_code = 102)
$sql_ins_fd = "INSERT INTO ".MAIN_DB_PREFIX."facturedet";
$sql_ins_fd .= " (fk_facture, description, qty, subprice, total_ht, total_tva, total_ttc,";
$sql_ins_fd .= " tva_tx, product_type, special_code, rang, info_bits)";
// Füge Subtotal in Detail-Tabelle ein (special_code = 102)
$sql_ins_fd = "INSERT INTO ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_ins_fd .= " (".$tables['fk_parent'].", description, qty, subprice, total_ht, total_tva, total_ttc,";
$sql_ins_fd .= " tva_tx, product_type, special_code, rang, info_bits,";
$sql_ins_fd .= " multicurrency_subprice, multicurrency_total_ht, multicurrency_total_tva, multicurrency_total_ttc)";
$sql_ins_fd .= " VALUES (";
$sql_ins_fd .= (int)$facture_id.", ";
$sql_ins_fd .= "'".$db->escape('Zwischensumme: '.$section->title)."', ";
@ -105,17 +133,22 @@ if ($show) {
$sql_ins_fd .= "9, "; // product_type = 9 (Titel/Kommentar)
$sql_ins_fd .= "102, "; // special_code = 102 (Subtotal)
$sql_ins_fd .= (int)$new_rang.", ";
$sql_ins_fd .= "0)";
$sql_ins_fd .= "0, "; // info_bits
$sql_ins_fd .= (float)$subtotal_ht.", "; // multicurrency_subprice
$sql_ins_fd .= (float)$subtotal_ht.", "; // multicurrency_total_ht
$sql_ins_fd .= "0, "; // multicurrency_total_tva
$sql_ins_fd .= (float)$subtotal_ht.")";
$db->query($sql_ins_fd);
$new_facturedet_id = $db->last_insert_id(MAIN_DB_PREFIX."facturedet");
$new_detail_id = $db->last_insert_id(MAIN_DB_PREFIX.$tables['lines_table']);
// Verknüpfe Manager-Eintrag mit facturedet
// Verknüpfe Manager-Eintrag mit Detail-Tabelle
$sql_link = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_link .= " SET fk_facturedet = ".(int)$new_facturedet_id;
$sql_link .= " SET ".$tables['fk_line']." = ".(int)$new_detail_id;
$sql_link .= ", in_facturedet = 1";
$sql_link .= " WHERE rowid = ".(int)$subtotal_manager_id;
$db->query($sql_link);
subtotaltitle_debug_log('✅ Subtotal-Zeile erstellt für Section ' . $section_id . ' mit Summe ' . $subtotal_ht . ' (facturedet #' . $new_facturedet_id . ', special_code=102)');
subtotaltitle_debug_log('✅ Subtotal-Zeile erstellt für Section ' . $section_id . ' mit Summe ' . $subtotal_ht . ' ('.$tables['lines_table'].' #' . $new_detail_id . ', special_code=102)');
}
} else {
// Hole erst fk_facturedet bevor wir löschen

View file

@ -0,0 +1,179 @@
<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* \file subtotaltitle/class/DocumentTypeHelper.class.php
* \ingroup subtotaltitle
* \brief Helper class für verschiedene Dokumenttypen (Rechnung, Angebot, Auftrag)
*/
/**
* Class DocumentTypeHelper
* Hilfsklasse um mit verschiedenen Dokumenttypen zu arbeiten
*/
class DocumentTypeHelper
{
/**
* Erkennt den Dokumenttyp aus dem Context
*
* @param string $context Hook-Context (z.B. 'invoicecard', 'propalcard', 'ordercard')
* @return string|null Dokumenttyp ('invoice', 'propal', 'order') oder null
*/
public static function getTypeFromContext($context)
{
if (strpos($context, 'invoicecard') !== false) {
return 'invoice';
}
if (strpos($context, 'propalcard') !== false) {
return 'propal';
}
if (strpos($context, 'ordercard') !== false) {
return 'order';
}
return null;
}
/**
* Holt den Dokumenttyp aus dem Object
*
* @param object $object Dolibarr Objekt
* @return string|null Dokumenttyp ('invoice', 'propal', 'order') oder null
*/
public static function getTypeFromObject($object)
{
if (!is_object($object) || !isset($object->element)) {
return null;
}
if ($object->element == 'facture') {
return 'invoice';
}
if ($object->element == 'propal') {
return 'propal';
}
if ($object->element == 'commande') {
return 'order';
}
return null;
}
/**
* Holt die Tabellennamen für einen Dokumenttyp
*
* @param string $type Dokumenttyp ('invoice', 'propal', 'order')
* @return array Array mit Tabellennamen (parent_table, lines_table, fk_parent, fk_line)
*/
public static function getTableNames($type)
{
$tables = array(
'invoice' => array(
'parent_table' => 'facture',
'lines_table' => 'facturedet',
'fk_parent' => 'fk_facture',
'fk_line' => 'fk_facturedet',
'element' => 'facture'
),
'propal' => array(
'parent_table' => 'propal',
'lines_table' => 'propaldet',
'fk_parent' => 'fk_propal',
'fk_line' => 'fk_propaldet',
'element' => 'propal'
),
'order' => array(
'parent_table' => 'commande',
'lines_table' => 'commandedet',
'fk_parent' => 'fk_commande',
'fk_line' => 'fk_commandedet',
'element' => 'commande'
)
);
return isset($tables[$type]) ? $tables[$type] : null;
}
/**
* Holt die Hook-Contexts für einen Dokumenttyp
*
* @param string $type Dokumenttyp ('invoice', 'propal', 'order')
* @return string Hook-Context
*/
public static function getContext($type)
{
$contexts = array(
'invoice' => 'invoicecard',
'propal' => 'propalcard',
'order' => 'ordercard'
);
return isset($contexts[$type]) ? $contexts[$type] : '';
}
/**
* Laedt ein Dokument basierend auf Typ und ID
*
* @param string $type Dokumenttyp ('invoice', 'propal', 'order')
* @param int $id Dokument-ID
* @param DoliDB $db Datenbankverbindung
* @return object|null Dolibarr Objekt oder null
*/
public static function loadDocument($type, $id, $db)
{
$object = null;
if ($type == 'invoice') {
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
$object = new Facture($db);
} elseif ($type == 'propal') {
require_once DOL_DOCUMENT_ROOT.'/comm/propal/class/propal.class.php';
$object = new Propal($db);
} elseif ($type == 'order') {
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
$object = new Commande($db);
}
if ($object && $object->fetch($id) > 0) {
return $object;
}
return null;
}
/**
* Prueft ob ein Dokument im Entwurfsstatus ist
*
* @param object $object Dolibarr Objekt
* @param string $type Dokumenttyp ('invoice', 'propal', 'order')
* @return bool true wenn Entwurf, sonst false
*/
public static function isDraft($object, $type)
{
if (!$object) {
return false;
}
// Verschiedene Dokumenttypen haben unterschiedliche Status-Felder
if (isset($object->statut)) {
return ($object->statut == 0);
}
if (isset($object->status)) {
return ($object->status == 0);
}
return false;
}
}

1212
class/actions_subtotaltitle.class.php Normal file → Executable file

File diff suppressed because it is too large Load diff

View file

@ -66,9 +66,9 @@ class modSubtotalTitle extends DolibarrModules
// DESCRIPTION_FLAG
// Module description, used if translation string 'ModuleSubtotalTitleDesc' not found (SubtotalTitle is name of module).
$this->description = "Positionsgruppen und Zwischensummen für Rechnungen";
$this->description = "Positionsgruppen und Zwischensummen für Rechnungen, Angebote und Kundenaufträge";
// Used only if file README.md and README-LL.md not found.
$this->descriptionlong = "Organisieren Sie Rechnungspositionen in Sections mit automatischen Zwischensummen. Fügen Sie Textzeilen hinzu und verwalten Sie komplexe Rechnungen übersichtlich.";
$this->descriptionlong = "Organisieren Sie Positionen in Sections mit automatischen Zwischensummen. Fügen Sie Textzeilen hinzu und verwalten Sie komplexe Dokumente übersichtlich. Unterstützt Rechnungen, Angebote und Kundenaufträge.";
// Author
$this->editor_name = 'Alles Watt läuft (Testsystem)';
@ -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 = '1.0';
$this->version = '4.1';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
@ -122,6 +122,8 @@ class modSubtotalTitle extends DolibarrModules
/* BEGIN MODULEBUILDER HOOKSCONTEXTS */
'hooks' => array(
'invoicecard',
'propalcard',
'ordercard',
),
/* END MODULEBUILDER HOOKSCONTEXTS */
// Set this to 1 if features of module are opened to external users

0
core/substitutions/functions_subtotaltitle.lib.php Normal file → Executable file
View file

View file

@ -78,8 +78,11 @@ tr.textline-row {
tr.textline-row .linecolmove {
cursor: move;
min-width: 20px;
}
/* Drag-Handle wird über JavaScript als background-image gesetzt (wie Dolibarr) */
tr.textline-row:hover {
background-color: #f5f5f5 !important;
}

0
css/subtotaltitle_sync.css Normal file → Executable file
View file

64
debug_sections.php Executable file
View file

@ -0,0 +1,64 @@
<?php
// Debug-Script um Sections zu prüfen
require_once '../../main.inc.php';
require_once __DIR__.'/class/DocumentTypeHelper.class.php';
$order_id = 18; // Die Order-ID aus der Konsole
$docType = 'order';
$tables = DocumentTypeHelper::getTableNames($docType);
echo "<h2>Debug: Sections für Order ID $order_id</h2>";
// SQL-Abfrage 1: Alle Einträge für diese Order
$sql1 = "SELECT * FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql1 .= " WHERE ".$tables['fk_parent']." = ".(int)$order_id;
$sql1 .= " ORDER BY line_order";
echo "<h3>SQL 1: Alle Einträge</h3>";
echo "<pre>".htmlspecialchars($sql1)."</pre>";
$resql1 = $db->query($sql1);
if ($resql1) {
echo "<table border='1'><tr><th>rowid</th><th>fk_commande</th><th>document_type</th><th>line_type</th><th>title</th><th>line_order</th></tr>";
while ($obj = $db->fetch_object($resql1)) {
echo "<tr>";
echo "<td>".$obj->rowid."</td>";
echo "<td>".$obj->fk_commande."</td>";
echo "<td>".$obj->document_type."</td>";
echo "<td>".$obj->line_type."</td>";
echo "<td>".$obj->title."</td>";
echo "<td>".$obj->line_order."</td>";
echo "</tr>";
}
echo "</table>";
} else {
echo "<p style='color:red'>Fehler: ".$db->lasterror()."</p>";
}
// SQL-Abfrage 2: Nur Sections mit document_type
$sql2 = "SELECT * FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql2 .= " WHERE ".$tables['fk_parent']." = ".(int)$order_id;
$sql2 .= " AND document_type = '".$db->escape($docType)."'";
$sql2 .= " AND line_type = 'section'";
$sql2 .= " ORDER BY line_order";
echo "<h3>SQL 2: Sections mit document_type = 'order'</h3>";
echo "<pre>".htmlspecialchars($sql2)."</pre>";
$resql2 = $db->query($sql2);
if ($resql2) {
$num = $db->num_rows($resql2);
echo "<p><strong>Anzahl gefunden: $num</strong></p>";
echo "<table border='1'><tr><th>rowid</th><th>title</th><th>line_order</th></tr>";
while ($obj = $db->fetch_object($resql2)) {
echo "<tr>";
echo "<td>".$obj->rowid."</td>";
echo "<td>".$obj->title."</td>";
echo "<td>".$obj->line_order."</td>";
echo "</tr>";
}
echo "</table>";
} else {
echo "<p style='color:red'>Fehler: ".$db->lasterror()."</p>";
}

BIN
img/grip.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

View file

@ -3,7 +3,7 @@ $(document).ready(function() {
var factureId = getFactureId();
if (!factureId) return;
$.get('/dolibarr/custom/subtotaltitle/ajax/get_line_orders.php', {
$.get(subtotaltitleAjaxUrl + 'get_line_orders.php', {
facture_id: factureId
}, function(response) {
if (!response.success) return;

File diff suppressed because it is too large Load diff

260
js/subtotaltitle_sync.js Normal file → Executable file
View file

@ -3,6 +3,20 @@
// Für Synchronisation mit facturedet
// ==========================================
/**
* Erkenne Dokumenttyp aus URL (lokale Kopie für Sync)
*/
function getDocumentTypeForSync() {
var url = window.location.href;
var docType = 'invoice'; // Default
if (url.indexOf('/comm/propal/') !== -1) {
docType = 'propal';
} else if (url.indexOf('/commande/') !== -1) {
docType = 'order';
}
return docType;
}
/**
* Synchronisiert eine Zeile mit facturedet (hinzufügen)
*/
@ -10,23 +24,27 @@ function syncToFacturedet(lineId, lineType) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
debugLog('📤 Sync to facturedet: ' + lineType + ' #' + lineId);
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
var docType = getDocumentTypeForSync();
debugLog('Document type: ' + docType);
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
action: 'add',
line_id: lineId,
line_type: lineType
line_type: lineType,
document_type: docType
}, function(response) {
debugLog('Sync response: ' + JSON.stringify(response));
if (response.success) {
updateSyncCheckbox(lineId, true);
debugLog('✅ Zeile zu Rechnung hinzugefügt');
} else {
alert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
// Checkbox zurücksetzen
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', false);
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorSyncing || 'Fehler beim Synchronisieren') + ': ' + error);
showErrorAlert((lang.errorSyncing || 'Fehler beim Synchronisieren') + ': ' + error);
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', false);
});
}
@ -36,32 +54,42 @@ function syncToFacturedet(lineId, lineType) {
*/
function removeFromFacturedet(lineId, lineType) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmRemoveLine || 'Zeile aus der Rechnung entfernen?\n\nDie Zeile bleibt in der Positionsgruppen-Verwaltung erhalten.')) {
// Checkbox zurücksetzen
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', true);
return;
}
debugLog('📥 Remove from facturedet: ' + lineType + ' #' + lineId);
showConfirmDialog(
'Aus Rechnung entfernen',
lang.confirmRemoveLine || 'Zeile aus der Rechnung entfernen?<br><br><em>Die Zeile bleibt in der Positionsgruppen-Verwaltung erhalten.</em>',
function() {
debugLog('📥 Remove from facturedet: ' + lineType + ' #' + lineId);
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
action: 'remove',
line_id: lineId,
line_type: lineType
}, function(response) {
debugLog('Remove response: ' + JSON.stringify(response));
if (response.success) {
updateSyncCheckbox(lineId, false);
debugLog('✅ Zeile aus Rechnung entfernt');
} else {
alert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', true);
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorSyncing || 'Fehler') + ': ' + error);
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', true);
});
var docType = getDocumentTypeForSync();
debugLog('Document type: ' + docType);
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
action: 'remove',
line_id: lineId,
line_type: lineType,
document_type: docType
}, function(response) {
debugLog('Remove response: ' + JSON.stringify(response));
if (response.success) {
updateSyncCheckbox(lineId, false);
debugLog('✅ Zeile aus Rechnung entfernt');
} else {
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', true);
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + error);
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', true);
});
},
'Ja, entfernen',
'Abbrechen'
);
// Checkbox zurücksetzen bis Bestätigung
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', true);
}
/**
@ -69,7 +97,7 @@ function removeFromFacturedet(lineId, lineType) {
*/
function toggleFacturedetSync(lineId, lineType, checkbox) {
if (event) event.stopPropagation();
if (checkbox.checked) {
syncToFacturedet(lineId, lineType);
} else {
@ -83,7 +111,7 @@ function toggleFacturedetSync(lineId, lineType, checkbox) {
function updateSyncCheckbox(lineId, isInFacturedet) {
var $checkbox = $('.sync-checkbox[data-line-id="' + lineId + '"]');
$checkbox.prop('checked', isInFacturedet);
var $row = $checkbox.closest('tr');
if (isInFacturedet) {
$row.addClass('in-facturedet');
@ -97,107 +125,106 @@ function updateSyncCheckbox(lineId, isInFacturedet) {
*/
function syncAllToFacturedet() {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmSyncAll || 'Alle Positionsgruppen-Elemente (Sections, Textzeilen, Zwischensummen) zur Rechnung hinzufügen?')) {
return;
}
debugLog('📤 Sync ALL to facturedet...');
var $unchecked = $('.sync-checkbox:not(:checked)');
var total = $unchecked.length;
var done = 0;
var errors = 0;
if (total === 0) {
alert(lang.allElementsAlreadyInInvoice || 'Alle Elemente sind bereits in der Rechnung.');
showErrorAlert(lang.allElementsAlreadyInInvoice || 'Alle Elemente sind bereits in der Rechnung.');
return;
}
$unchecked.each(function() {
var lineId = $(this).data('line-id');
var lineType = $(this).data('line-type');
showConfirmDialog(
'Alle zur Rechnung hinzufügen',
lang.confirmSyncAll || 'Alle Positionsgruppen-Elemente (Sections, Textzeilen, Zwischensummen) zur Rechnung hinzufügen?',
function() {
debugLog('📤 Sync ALL to facturedet...');
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
action: 'add',
line_id: lineId,
line_type: lineType
}, function(response) {
done++;
if (response.success) {
updateSyncCheckbox(lineId, true);
} else {
errors++;
}
if (done >= total) {
debugLog('✅ Sync abgeschlossen: ' + (total - errors) + ' erfolgreich, ' + errors + ' Fehler');
if (errors > 0) {
var msg = (lang.elementsAddedWithErrors || '%s von %s Elementen hinzugefügt.\n%s Fehler aufgetreten.')
.replace('%s', total - errors).replace('%s', total).replace('%s', errors);
alert(msg);
} else {
var msg = (lang.elementsAddedToInvoice || '%s Elemente zur Rechnung hinzugefügt.').replace('%s', total);
alert(msg);
}
}
}, 'json').fail(function() {
done++;
errors++;
});
});
var done = 0;
var errors = 0;
var docType = getDocumentTypeForSync();
// Zeige Loading-Hinweis
$('body').append('<div id="sync-loading" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.3);z-index:9999;display:flex;align-items:center;justify-content:center;"><div style="background:#fff;padding:20px 40px;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,0.2);">Synchronisiere... Bitte warten.</div></div>');
$unchecked.each(function() {
var lineId = $(this).data('line-id');
var lineType = $(this).data('line-type');
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
action: 'add',
line_id: lineId,
line_type: lineType,
document_type: docType
}, function(response) {
done++;
if (!response.success) {
errors++;
}
if (done >= total) {
debugLog('✅ Sync abgeschlossen: ' + (total - errors) + ' erfolgreich, ' + errors + ' Fehler');
if (errors > 0) {
$('#sync-loading').remove();
showErrorAlert((total - errors) + ' von ' + total + ' Elementen hinzugefügt. ' + errors + ' Fehler aufgetreten.');
} else {
// Direkt reloaden ohne UI-Update
safeReload();
}
}
}, 'json').fail(function() {
done++;
errors++;
});
});
},
'Ja, hinzufügen',
'Abbrechen'
);
}
/**
* Entfernt ALLE Sections/Textzeilen/Subtotals aus facturedet
* Inkl. verwaister Einträge die nicht mehr in der Manager-Tabelle existieren
*/
function removeAllFromFacturedet() {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmRemoveAll || 'ALLE Positionsgruppen-Elemente aus der Rechnung entfernen?\n\nDie Elemente bleiben in der Verwaltung erhalten.')) {
return;
}
debugLog('📥 Remove ALL from facturedet...');
showConfirmDialog(
'Alle aus Rechnung entfernen',
(lang.confirmRemoveAll || 'ALLE Positionsgruppen-Elemente (Sections, Textzeilen, Zwischensummen) aus der Rechnung entfernen?') +
'<br><br><em>Inkl. verwaister Einträge. Die Elemente in der Verwaltung bleiben erhalten.</em>',
function() {
debugLog('📥 Remove ALL from facturedet (server-side)...');
var $checked = $('.sync-checkbox:checked');
var total = $checked.length;
var done = 0;
var errors = 0;
var docType = getDocumentTypeForSync();
var documentId = getFactureId();
if (total === 0) {
alert(lang.noElementsInInvoice || 'Keine Elemente in der Rechnung vorhanden.');
return;
}
$checked.each(function() {
var lineId = $(this).data('line-id');
var lineType = $(this).data('line-type');
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
action: 'remove',
line_id: lineId,
line_type: lineType
}, function(response) {
done++;
if (response.success) {
updateSyncCheckbox(lineId, false);
} else {
errors++;
if (!documentId) {
showErrorAlert('Fehler: Keine Dokument-ID gefunden');
return;
}
if (done >= total) {
debugLog('✅ Remove abgeschlossen: ' + (total - errors) + ' erfolgreich, ' + errors + ' Fehler');
if (errors > 0) {
var msg = (lang.elementsRemovedWithErrors || '%s von %s Elementen entfernt.\n%s Fehler aufgetreten.')
.replace('%s', total - errors).replace('%s', total).replace('%s', errors);
alert(msg);
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
action: 'remove_all',
line_id: 1, // Dummy, wird benötigt wegen Parameter-Check
document_id: documentId,
document_type: docType
}, function(response) {
debugLog('Remove ALL response: ' + JSON.stringify(response));
if (response.success) {
debugLog('✅ Alle Spezialzeilen entfernt');
safeReload();
} else {
var msg = (lang.elementsRemovedFromInvoice || '%s Elemente aus Rechnung entfernt.').replace('%s', total);
alert(msg);
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}
}, 'json').fail(function() {
done++;
errors++;
});
});
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + error);
});
},
'Ja, alle entfernen',
'Abbrechen'
);
}
/**
@ -205,22 +232,25 @@ function removeAllFromFacturedet() {
*/
function updateAllSubtotals() {
debugLog('🔄 Update all subtotals...');
var $subtotals = $('.sync-checkbox[data-line-type="subtotal"]:checked');
var total = $subtotals.length;
var done = 0;
if (total === 0) {
debugLog('Keine Subtotals in facturedet');
return;
}
var docType = getDocumentTypeForSync();
$subtotals.each(function() {
var lineId = $(this).data('line-id');
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
action: 'update_subtotal',
line_id: lineId
line_id: lineId,
document_type: docType
}, function(response) {
done++;
debugLog('Subtotal #' + lineId + ' updated: ' + JSON.stringify(response));

19
langs/de_DE/subtotaltitle.lang Normal file → Executable file
View file

@ -85,10 +85,10 @@ ButtonCancel = Abbrechen
#
# UI Elements - Section Actions
#
SectionCreate = Section erstellen
SectionEdit = Section bearbeiten
SectionDelete = Section löschen
SectionName = Section-Name
SectionCreate = Produktgruppe erstellen
SectionEdit = Produktgruppe bearbeiten
SectionDelete = Produktgruppe löschen
SectionName = Name der Produktgruppe
SectionSubtotal = Zwischensumme anzeigen
ProductCount = Produkte
@ -143,3 +143,14 @@ ElementsRemovedWithErrors = %s von %s Elementen entfernt.\n%s Fehler aufgetreten
SuccessSyncedToInvoice = Erfolgreich zur Rechnung synchronisiert
SuccessRemovedFromInvoice = Erfolgreich aus Rechnung entfernt
ErrorSyncing = Fehler beim Synchronisieren
#
# UI Elements - Import from Origin
#
ImportFromOrigin = Produktgruppen importieren
ImportFromOriginTitle = Produktgruppen importieren
ImportFromOriginConfirm = Produktgruppen aus %origin% importieren?
ImportFromOriginSuccess = Import erfolgreich!
ImportFromOriginNoOrigin = Kein Ursprungsdokument verknüpft. Dieses Dokument wurde nicht aus einem Angebot oder Auftrag erstellt.
ImportFromOriginNoData = Das Ursprungsdokument enthält keine Produktgruppen oder Textzeilen zum Importieren.
ImportFromOriginError = Fehler beim Import

View file

@ -85,10 +85,10 @@ ButtonCancel = Cancel
#
# UI Elements - Section Actions
#
SectionCreate = Create section
SectionEdit = Edit section
SectionDelete = Delete section
SectionName = Section name
SectionCreate = Create product group
SectionEdit = Edit product group
SectionDelete = Delete product group
SectionName = Product group name
SectionSubtotal = Show subtotal
ProductCount = Products
@ -143,3 +143,14 @@ ElementsRemovedWithErrors = %s of %s elements removed.\n%s errors occurred.
SuccessSyncedToInvoice = Successfully synced to invoice
SuccessRemovedFromInvoice = Successfully removed from invoice
ErrorSyncing = Error syncing
#
# UI Elements - Import from Origin
#
ImportFromOrigin = Import product groups
ImportFromOriginTitle = Import product groups
ImportFromOriginConfirm = Import product groups from %origin%?
ImportFromOriginSuccess = Import successful!
ImportFromOriginNoOrigin = No origin document linked. This document was not created from a proposal or order.
ImportFromOriginNoData = The origin document contains no product groups or text lines to import.
ImportFromOriginError = Error during import

View file

@ -0,0 +1,51 @@
-- ============================================================================
-- Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
-- ============================================================================
CREATE TABLE IF NOT EXISTS llx_facture_lines_manager (
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
-- Verknüpfung mit dem Elterndokument (nur eines davon ist gesetzt)
fk_facture INTEGER DEFAULT NULL, -- Rechnung
fk_propal INTEGER DEFAULT NULL, -- Angebot
fk_commande INTEGER DEFAULT NULL, -- Kundenauftrag
-- Dokumenttyp zur einfachen Unterscheidung
document_type VARCHAR(20) DEFAULT 'invoice', -- 'invoice', 'propal', 'order'
-- Zeilentyp
line_type VARCHAR(20) NOT NULL, -- 'section', 'product', 'text', 'subtotal'
-- Verknüpfung zur Detail-Tabelle (facturedet, propaldet, commandedet)
fk_facturedet INTEGER DEFAULT NULL, -- für Rechnungen
fk_propaldet INTEGER DEFAULT NULL, -- für Angebote
fk_commandedet INTEGER DEFAULT NULL, -- für Kundenaufträge
-- Section-Eigenschaften
title VARCHAR(255) DEFAULT NULL,
parent_section INTEGER DEFAULT NULL, -- FK zu rowid dieser Tabelle
-- Sortierung und Status
line_order INTEGER DEFAULT 0,
show_subtotal TINYINT DEFAULT 0, -- Zwischensumme anzeigen?
collapsed TINYINT DEFAULT 0, -- Section eingeklappt?
in_facturedet TINYINT DEFAULT 0, -- In der Detail-Tabelle vorhanden?
-- Zeitstempel
date_creation DATETIME DEFAULT NULL,
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Indizes
INDEX idx_flm_fk_facture (fk_facture),
INDEX idx_flm_fk_propal (fk_propal),
INDEX idx_flm_fk_commande (fk_commande),
INDEX idx_flm_document_type (document_type),
INDEX idx_flm_line_type (line_type),
INDEX idx_flm_parent_section (parent_section),
INDEX idx_flm_line_order (line_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View file

@ -1,4 +0,0 @@
[Project]
CreatedFrom=
Manager=KDevCustomBuildSystem
Name=subtotaltitle