Stable Version fertig Multidocument Kundenaufträge Angebot und Rechnungen
This commit is contained in:
parent
a1468d359e
commit
870563a1d7
10 changed files with 672 additions and 172 deletions
209
README.md
209
README.md
|
|
@ -1,66 +1,41 @@
|
||||||
# SubtotalTitle - Facturedet Sync Update
|
# SubtotalTitle - Dolibarr Modul
|
||||||
|
|
||||||
## Was ist neu?
|
Erweitert Rechnungen, Angebote und Kundenaufträge um **Sections**, **Textzeilen** und **Zwischensummen**.
|
||||||
- **📄 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
|
|
||||||
|
|
||||||
## Installation
|
---
|
||||||
|
|
||||||
### 1. Dateien kopieren
|
## 🔑 ODT Template Schlüsselwörter
|
||||||
|
|
||||||
```
|
Diese Variablen stehen in ODT-Templates zur Verfügung:
|
||||||
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!)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Modul-Descriptor anpassen
|
| Variable | Wert | Beschreibung |
|
||||||
|
|----------|------|--------------|
|
||||||
|
| `{line_is_section}` | 1/0 | Zeile ist eine Section (Überschrift) |
|
||||||
|
| `{line_is_textline}` | 1/0 | Zeile ist eine Textzeile |
|
||||||
|
| `{line_is_subtotal}` | 1/0 | Zeile ist eine Zwischensumme |
|
||||||
|
| `{line_is_product}` | 1/0 | Zeile ist ein Produkt (inkl. Sections/Text/Subtotals) |
|
||||||
|
| `{line_is_normal}` | 1/0 | Zeile ist ein normales Produkt (KEIN Section/Text/Subtotal) |
|
||||||
|
| `{line_is_special}` | 1/0 | Zeile ist Section, Text ODER Subtotal |
|
||||||
|
| `{line_special_code}` | 0-102 | special_code Wert der Zeile |
|
||||||
|
|
||||||
In `core/modules/modSubtotalTitle.class.php` ändern:
|
### special_code Werte
|
||||||
|
|
||||||
```php
|
| Typ | special_code |
|
||||||
$this->module_parts = array(
|
|-----|-------------|
|
||||||
'substitutions' => 1, // ← Diese Zeile hinzufügen/ändern!
|
| Normales Produkt | 0 |
|
||||||
'hooks' => array(
|
| Section (Überschrift) | 100 |
|
||||||
'data' => array('invoicecard'),
|
| Textzeile | 101 |
|
||||||
'entity' => '0'
|
| Zwischensumme | 102 |
|
||||||
)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Modul deaktivieren und wieder aktivieren
|
### ODT Template Beispiel
|
||||||
|
|
||||||
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 --]
|
[!-- BEGIN row.lines --]
|
||||||
|
|
||||||
[!-- IF {line_is_section} --]
|
[!-- IF {line_is_section} --]
|
||||||
|
═══════════════════════════════════════
|
||||||
{line_desc}
|
{line_desc}
|
||||||
|
═══════════════════════════════════════
|
||||||
[!-- ENDIF {line_is_section} --]
|
[!-- ENDIF {line_is_section} --]
|
||||||
|
|
||||||
[!-- IF {line_is_textline} --]
|
[!-- IF {line_is_textline} --]
|
||||||
|
|
@ -72,17 +47,139 @@ Damit die Substitution-Funktion erkannt wird.
|
||||||
[!-- ENDIF {line_is_normal} --]
|
[!-- ENDIF {line_is_normal} --]
|
||||||
|
|
||||||
[!-- IF {line_is_subtotal} --]
|
[!-- IF {line_is_subtotal} --]
|
||||||
|
───────────────────────────────────────
|
||||||
Zwischensumme: {line_price_ht_locale} €
|
Zwischensumme: {line_price_ht_locale} €
|
||||||
|
───────────────────────────────────────
|
||||||
[!-- ENDIF {line_is_subtotal} --]
|
[!-- ENDIF {line_is_subtotal} --]
|
||||||
|
|
||||||
[!-- END row.lines --]
|
[!-- END row.lines --]
|
||||||
```
|
```
|
||||||
|
|
||||||
## special_code Werte
|
---
|
||||||
|
|
||||||
| Typ | special_code | ODT-Variable |
|
## 📋 Modul-Funktionen
|
||||||
|-----|-------------|--------------|
|
|
||||||
| Normales Produkt | 0 | `{line_is_normal}` |
|
### Sections (Überschriften)
|
||||||
| Section | 100 | `{line_is_section}` |
|
- Erstellen von Überschriften zur Strukturierung
|
||||||
| Textzeile | 101 | `{line_is_textline}` |
|
- Produkte können per Drag & Drop oder Link-Button zugeordnet werden
|
||||||
| Zwischensumme | 102 | `{line_is_subtotal}` |
|
- 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.
|
||||||
|
|
|
||||||
|
|
@ -58,38 +58,40 @@ if (!$resql_section || $db->num_rows($resql_section) == 0) {
|
||||||
$section = $db->fetch_object($resql_section);
|
$section = $db->fetch_object($resql_section);
|
||||||
$section_line_order = $section->line_order;
|
$section_line_order = $section->line_order;
|
||||||
|
|
||||||
// 3. Finde die neue line_order für das Produkt (nach der Section, vor anderen Produkten der Section)
|
// 3. Finde die neue line_order für das Produkt
|
||||||
$sql_next = "SELECT MIN(line_order) as next_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
// Das Produkt soll IMMER als LETZTES Produkt der Section eingefügt werden
|
||||||
$sql_next .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
// (nach allen bestehenden Produkten dieser Section, aber vor der nächsten Section oder dem Subtotal)
|
||||||
$sql_next .= " AND document_type = '".$db->escape($docType)."'";
|
|
||||||
$sql_next .= " AND parent_section = ".(int)$section_id;
|
|
||||||
$sql_next .= " AND line_type IN ('product', 'text')";
|
|
||||||
$resql_next = $db->query($sql_next);
|
|
||||||
$obj_next = $db->fetch_object($resql_next);
|
|
||||||
|
|
||||||
if ($obj_next && $obj_next->next_order) {
|
// Suche das letzte Produkt/Text dieser Section
|
||||||
// Es gibt bereits Produkte in dieser Section → füge VOR dem ersten ein
|
$sql_last = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
$new_line_order = $obj_next->next_order;
|
$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 {
|
} else {
|
||||||
// Keine Produkte in der Section → füge direkt nach der Section ein
|
// Keine anderen Produkte in der Section → füge direkt nach der Section ein
|
||||||
$new_line_order = $section_line_order + 1;
|
$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);
|
subtotaltitle_debug_log(' current_line_order='.$current_line_order.', section_line_order='.$section_line_order.', new_line_order='.$new_line_order);
|
||||||
|
|
||||||
// 4. Verschiebe Zeilen je nach Richtung
|
// 4. Verschiebe Zeilen
|
||||||
if ($current_line_order > $new_line_order) {
|
// WICHTIG: Zuerst das Produkt "entfernen" (temporär auf -1 setzen), dann verschieben, dann einfügen
|
||||||
// Nach vorne: Verschiebe Zeilen von new_line_order bis BEFORE current um +1
|
$sql_temp = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
$sql_shift = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
|
$sql_temp .= " SET line_order = -1";
|
||||||
$sql_shift .= " SET line_order = line_order + 1";
|
$sql_temp .= " WHERE rowid = ".(int)$manager_id;
|
||||||
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
$db->query($sql_temp);
|
||||||
$sql_shift .= " AND document_type = '".$db->escape($docType)."'";
|
|
||||||
$sql_shift .= " AND line_order >= ".(int)$new_line_order;
|
if ($current_line_order < $new_line_order) {
|
||||||
$sql_shift .= " AND line_order < ".(int)$current_line_order;
|
// Produkt wird nach hinten verschoben
|
||||||
$db->query($sql_shift);
|
// Schließe die Lücke: alle Zeilen zwischen current+1 und new_line_order um -1 verschieben
|
||||||
subtotaltitle_debug_log(' Nach vorne verschoben: Zeilen '.$new_line_order.'-'.($current_line_order-1).' um +1');
|
|
||||||
} elseif ($current_line_order < $new_line_order) {
|
|
||||||
// Nach hinten: Verschiebe Zeilen von current+1 bis new um -1
|
|
||||||
$sql_shift = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
|
$sql_shift = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
$sql_shift .= " SET line_order = line_order - 1";
|
$sql_shift .= " SET line_order = line_order - 1";
|
||||||
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
||||||
|
|
@ -97,9 +99,21 @@ if ($current_line_order > $new_line_order) {
|
||||||
$sql_shift .= " AND line_order > ".(int)$current_line_order;
|
$sql_shift .= " AND line_order > ".(int)$current_line_order;
|
||||||
$sql_shift .= " AND line_order <= ".(int)$new_line_order;
|
$sql_shift .= " AND line_order <= ".(int)$new_line_order;
|
||||||
$db->query($sql_shift);
|
$db->query($sql_shift);
|
||||||
subtotaltitle_debug_log(' Nach hinten verschoben: Zeilen '.($current_line_order+1).'-'.$new_line_order.' um -1');
|
// 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);
|
||||||
}
|
}
|
||||||
// Wenn current == new, keine Verschiebung nötig
|
|
||||||
|
|
||||||
// 5. Update das Produkt: setze parent_section und neue line_order
|
// 5. Update das Produkt: setze parent_section und neue line_order
|
||||||
$sql_update = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
|
$sql_update = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
|
@ -108,9 +122,26 @@ $sql_update .= ", line_order = ".(int)$new_line_order;
|
||||||
$sql_update .= " WHERE rowid = ".(int)$manager_id;
|
$sql_update .= " WHERE rowid = ".(int)$manager_id;
|
||||||
$db->query($sql_update);
|
$db->query($sql_update);
|
||||||
|
|
||||||
subtotaltitle_debug_log('✅ Produkt #'.$line_id.' zu Section #'.$section_id.' hinzugefügt');
|
subtotaltitle_debug_log('✅ Produkt #'.$line_id.' zu Section #'.$section_id.' hinzugefügt mit line_order='.$new_line_order);
|
||||||
|
|
||||||
// 6. Sync rang in Detail-Tabelle
|
// 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);
|
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 = "SELECT rowid, line_type, ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ $sql_fix_sections .= " WHERE line_type = 'section'";
|
||||||
$sql_fix_sections .= " AND parent_section IS NOT NULL";
|
$sql_fix_sections .= " AND parent_section IS NOT NULL";
|
||||||
$sql_fix_sections .= " AND ".$tables['fk_parent']." = ".(int)$facture_id;
|
$sql_fix_sections .= " AND ".$tables['fk_parent']." = ".(int)$facture_id;
|
||||||
$sql_fix_sections .= " AND document_type = '".$db->escape($docType)."'";
|
$sql_fix_sections .= " AND document_type = '".$db->escape($docType)."'";
|
||||||
$db->query($sql_fix_sections);
|
$resql_fix = $db->query($sql_fix_sections);
|
||||||
$sections_fixed = $db->affected_rows();
|
$sections_fixed = $resql_fix ? $db->affected_rows($resql_fix) : 0;
|
||||||
if ($sections_fixed > 0) {
|
if ($sections_fixed > 0) {
|
||||||
subtotaltitle_debug_log('🧹 ' . $sections_fixed . ' Sections mit falscher parent_section korrigiert');
|
subtotaltitle_debug_log('🧹 ' . $sections_fixed . ' Sections mit falscher parent_section korrigiert');
|
||||||
$fixed += $sections_fixed;
|
$fixed += $sections_fixed;
|
||||||
|
|
@ -45,8 +45,8 @@ $sql_fix_zero .= " SET parent_section = NULL";
|
||||||
$sql_fix_zero .= " WHERE parent_section = 0";
|
$sql_fix_zero .= " WHERE parent_section = 0";
|
||||||
$sql_fix_zero .= " AND ".$tables['fk_parent']." = ".(int)$facture_id;
|
$sql_fix_zero .= " AND ".$tables['fk_parent']." = ".(int)$facture_id;
|
||||||
$sql_fix_zero .= " AND document_type = '".$db->escape($docType)."'";
|
$sql_fix_zero .= " AND document_type = '".$db->escape($docType)."'";
|
||||||
$db->query($sql_fix_zero);
|
$resql_fix_zero = $db->query($sql_fix_zero);
|
||||||
$zero_fixed = $db->affected_rows();
|
$zero_fixed = $resql_fix_zero ? $db->affected_rows($resql_fix_zero) : 0;
|
||||||
if ($zero_fixed > 0) {
|
if ($zero_fixed > 0) {
|
||||||
subtotaltitle_debug_log('🧹 ' . $zero_fixed . ' Einträge mit parent_section=0 korrigiert');
|
subtotaltitle_debug_log('🧹 ' . $zero_fixed . ' Einträge mit parent_section=0 korrigiert');
|
||||||
$fixed += $zero_fixed;
|
$fixed += $zero_fixed;
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ $fk_commande = ($docType === 'order') ? (int)$facture_id : 'NULL';
|
||||||
|
|
||||||
$sql = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
$sql .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title, line_order, in_facturedet, date_creation)";
|
$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.", 1, NOW())";
|
$sql .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'text', '".$db->escape($text)."', ".$next_order.", 0, NOW())";
|
||||||
|
|
||||||
if ($db->query($sql)) {
|
if ($db->query($sql)) {
|
||||||
$new_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");
|
$new_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,37 @@ foreach ($subtotals_to_update as $subtotal) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 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.')');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========== ALLES NEU DURCHNUMMERIEREN ==========
|
// ========== ALLES NEU DURCHNUMMERIEREN ==========
|
||||||
subtotaltitle_debug_log('🔢 Normalisiere line_order...');
|
subtotaltitle_debug_log('🔢 Normalisiere line_order...');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -141,41 +141,17 @@ if ($action == 'add') {
|
||||||
// Bestimme rang (Position) - UNTERSCHIEDLICH für Sections vs andere Zeilen
|
// Bestimme rang (Position) - UNTERSCHIEDLICH für Sections vs andere Zeilen
|
||||||
$new_rang = 1;
|
$new_rang = 1;
|
||||||
|
|
||||||
if ($line_type == 'section') {
|
// EINHEITLICHE BERECHNUNG für ALLE Zeilentypen basierend auf line_order
|
||||||
// Für Sections: Finde das erste Produkt dieser Section und füge Section DAVOR ein
|
// Finde den höchsten rang aller Zeilen, die VOR dieser Zeile (nach line_order) liegen
|
||||||
$sql_first_product = "SELECT MIN(d.rang) as min_rang";
|
$sql_rang = "SELECT MAX(d.rang) as max_rang";
|
||||||
$sql_first_product .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
|
$sql_rang .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
|
||||||
$sql_first_product .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
|
$sql_rang .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
|
||||||
$sql_first_product .= " WHERE m.parent_section = ".(int)$line_id;
|
$sql_rang .= " WHERE m.".$tables['fk_parent']." = ".(int)$document_id;
|
||||||
$sql_first_product .= " AND m.".$tables['fk_parent']." = ".(int)$document_id;
|
$sql_rang .= " AND m.document_type = '".$db->escape($docType)."'";
|
||||||
$sql_first_product .= " AND m.document_type = '".$db->escape($docType)."'";
|
$sql_rang .= " AND m.line_order < ".(int)$line->line_order;
|
||||||
$sql_first_product .= " AND m.line_type = 'product'";
|
$res_rang = $db->query($sql_rang);
|
||||||
$res_first = $db->query($sql_first_product);
|
$obj_rang = $db->fetch_object($res_rang);
|
||||||
$obj_first = $db->fetch_object($res_first);
|
$new_rang = ($obj_rang && $obj_rang->max_rang) ? $obj_rang->max_rang + 1 : 1;
|
||||||
|
|
||||||
if ($obj_first && $obj_first->min_rang) {
|
|
||||||
// Section VOR dem ersten Produkt einfügen
|
|
||||||
$new_rang = (int)$obj_first->min_rang;
|
|
||||||
} else {
|
|
||||||
// Keine Produkte in dieser Section - ans Ende
|
|
||||||
$sql_max = "SELECT MAX(rang) as max_rang FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
|
|
||||||
$sql_max .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
|
||||||
$res_max = $db->query($sql_max);
|
|
||||||
$obj_max = $db->fetch_object($res_max);
|
|
||||||
$new_rang = ($obj_max && $obj_max->max_rang) ? $obj_max->max_rang + 1 : 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Für Text/Subtotal: Basierend auf line_order Position
|
|
||||||
$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.$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);
|
subtotaltitle_debug_log('📝 Berechne rang: line_type='.$line_type.', new_rang='.$new_rang);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,17 @@ if ($show) {
|
||||||
$res_last = $db->query($sql_last);
|
$res_last = $db->query($sql_last);
|
||||||
$obj_last = $db->fetch_object($res_last);
|
$obj_last = $db->fetch_object($res_last);
|
||||||
$last_order = $obj_last->max_order ? $obj_last->max_order : 0;
|
$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;
|
$new_order = $last_order + 1;
|
||||||
|
|
||||||
// Alle nachfolgenden Zeilen +1
|
// Alle nachfolgenden Zeilen +1
|
||||||
|
|
@ -88,13 +97,14 @@ if ($show) {
|
||||||
$subtotal_manager_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");
|
$subtotal_manager_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");
|
||||||
|
|
||||||
// Subtotal-Zeile auch direkt in Detail-Tabelle einfügen
|
// Subtotal-Zeile auch direkt in Detail-Tabelle einfügen
|
||||||
// Bestimme rang (nach letztem Produkt der Section)
|
// 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 = "SELECT MAX(d.rang) as max_rang";
|
||||||
$sql_rang .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
|
$sql_rang .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
|
||||||
$sql_rang .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
|
$sql_rang .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
|
||||||
$sql_rang .= " WHERE m.parent_section = ".(int)$section_id;
|
$sql_rang .= " WHERE m.".$tables['fk_parent']." = ".(int)$facture_id;
|
||||||
$sql_rang .= " AND m.document_type = '".$db->escape($docType)."'";
|
$sql_rang .= " AND m.document_type = '".$db->escape($docType)."'";
|
||||||
$sql_rang .= " AND m.line_type = 'product'";
|
$sql_rang .= " AND m.line_order < ".(int)$new_order;
|
||||||
$res_rang = $db->query($sql_rang);
|
$res_rang = $db->query($sql_rang);
|
||||||
$obj_rang = $db->fetch_object($res_rang);
|
$obj_rang = $db->fetch_object($res_rang);
|
||||||
$new_rang = ($obj_rang && $obj_rang->max_rang) ? $obj_rang->max_rang + 1 : 1;
|
$new_rang = ($obj_rang && $obj_rang->max_rang) ? $obj_rang->max_rang + 1 : 1;
|
||||||
|
|
@ -109,7 +119,8 @@ if ($show) {
|
||||||
// Füge Subtotal in Detail-Tabelle ein (special_code = 102)
|
// Füge Subtotal in Detail-Tabelle ein (special_code = 102)
|
||||||
$sql_ins_fd = "INSERT INTO ".MAIN_DB_PREFIX.$tables['lines_table'];
|
$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 .= " (".$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 .= " 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 .= " VALUES (";
|
||||||
$sql_ins_fd .= (int)$facture_id.", ";
|
$sql_ins_fd .= (int)$facture_id.", ";
|
||||||
$sql_ins_fd .= "'".$db->escape('Zwischensumme: '.$section->title)."', ";
|
$sql_ins_fd .= "'".$db->escape('Zwischensumme: '.$section->title)."', ";
|
||||||
|
|
@ -122,7 +133,11 @@ if ($show) {
|
||||||
$sql_ins_fd .= "9, "; // product_type = 9 (Titel/Kommentar)
|
$sql_ins_fd .= "9, "; // product_type = 9 (Titel/Kommentar)
|
||||||
$sql_ins_fd .= "102, "; // special_code = 102 (Subtotal)
|
$sql_ins_fd .= "102, "; // special_code = 102 (Subtotal)
|
||||||
$sql_ins_fd .= (int)$new_rang.", ";
|
$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);
|
$db->query($sql_ins_fd);
|
||||||
$new_detail_id = $db->last_insert_id(MAIN_DB_PREFIX.$tables['lines_table']);
|
$new_detail_id = $db->last_insert_id(MAIN_DB_PREFIX.$tables['lines_table']);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,11 +142,14 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
// Lade Übersetzungen
|
// Lade Übersetzungen
|
||||||
$langs->load('subtotaltitle@subtotaltitle');
|
$langs->load('subtotaltitle@subtotaltitle');
|
||||||
|
|
||||||
// Prüfe ob Sections existieren (für Collapse-Buttons)
|
// Prüfe ob Sections oder Textzeilen existieren (für Buttons)
|
||||||
global $db;
|
global $db;
|
||||||
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
|
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
|
||||||
$hasSections = false;
|
$hasSections = false;
|
||||||
|
$hasTextLines = false;
|
||||||
|
$hasSectionsOrTextLines = false;
|
||||||
if ($tables && $object->id) {
|
if ($tables && $object->id) {
|
||||||
|
// Prüfe Sections
|
||||||
$sql_sec = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
$sql_sec = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
$sql_sec .= " WHERE ".$tables['fk_parent']." = ".(int)$object->id;
|
$sql_sec .= " WHERE ".$tables['fk_parent']." = ".(int)$object->id;
|
||||||
$sql_sec .= " AND document_type = '".$db->escape($this->currentDocType)."'";
|
$sql_sec .= " AND document_type = '".$db->escape($this->currentDocType)."'";
|
||||||
|
|
@ -155,6 +158,18 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
if ($res_sec && $obj_sec = $db->fetch_object($res_sec)) {
|
if ($res_sec && $obj_sec = $db->fetch_object($res_sec)) {
|
||||||
$hasSections = ($obj_sec->cnt > 0);
|
$hasSections = ($obj_sec->cnt > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prüfe Textzeilen
|
||||||
|
$sql_text = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_text .= " WHERE ".$tables['fk_parent']." = ".(int)$object->id;
|
||||||
|
$sql_text .= " AND document_type = '".$db->escape($this->currentDocType)."'";
|
||||||
|
$sql_text .= " AND line_type = 'text'";
|
||||||
|
$res_text = $db->query($sql_text);
|
||||||
|
if ($res_text && $obj_text = $db->fetch_object($res_text)) {
|
||||||
|
$hasTextLines = ($obj_text->cnt > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasSectionsOrTextLines = ($hasSections || $hasTextLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSS
|
// CSS
|
||||||
|
|
@ -247,20 +262,23 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
echo '});</script>'."\n";
|
echo '});</script>'."\n";
|
||||||
|
|
||||||
// Sync-Buttons + Collapse-Buttons - rechts ausgerichtet
|
// Sync-Buttons + Collapse-Buttons - rechts ausgerichtet
|
||||||
echo '<script>$(document).ready(function() {';
|
// Nur anzeigen wenn Sections oder Textzeilen vorhanden sind
|
||||||
echo ' if ($(".sync-collapse-row").length === 0) {';
|
if ($hasSectionsOrTextLines) {
|
||||||
echo ' var buttons = \'<div class="sync-collapse-row" style="text-align:right; margin:5px 0;">\';';
|
echo '<script>$(document).ready(function() {';
|
||||||
echo ' buttons += \'<a class="button" href="#" onclick="syncAllToFacturedet(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonToInvoice + \'</a>\';';
|
echo ' if ($(".sync-collapse-row").length === 0) {';
|
||||||
echo ' buttons += \'<a class="button" href="#" onclick="removeAllFromFacturedet(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonFromInvoice + \'</a>\';';
|
echo ' var buttons = \'<div class="sync-collapse-row" style="text-align:right; margin:5px 0;">\';';
|
||||||
// Collapse-Buttons nur wenn Sections existieren (aus PHP)
|
echo ' buttons += \'<a class="button" href="#" onclick="syncAllToFacturedet(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonToInvoice + \'</a>\';';
|
||||||
if ($hasSections) {
|
echo ' buttons += \'<a class="button" href="#" onclick="removeAllFromFacturedet(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonFromInvoice + \'</a>\';';
|
||||||
echo ' buttons += \'<a class="button" href="#" onclick="expandAllSections(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonExpandAll + \'</a>\';';
|
// Collapse-Buttons nur wenn Sections existieren (aus PHP)
|
||||||
echo ' buttons += \'<a class="button" href="#" onclick="collapseAllSections(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonCollapseAll + \'</a>\';';
|
if ($hasSections) {
|
||||||
|
echo ' buttons += \'<a class="button" href="#" onclick="expandAllSections(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonExpandAll + \'</a>\';';
|
||||||
|
echo ' buttons += \'<a class="button" href="#" onclick="collapseAllSections(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonCollapseAll + \'</a>\';';
|
||||||
|
}
|
||||||
|
echo ' buttons += \'</div>\';';
|
||||||
|
echo ' $(".tabsAction").first().after(buttons);';
|
||||||
|
echo ' }';
|
||||||
|
echo '});</script>'."\n";
|
||||||
}
|
}
|
||||||
echo ' buttons += \'</div>\';';
|
|
||||||
echo ' $(".tabsAction").first().after(buttons);';
|
|
||||||
echo ' }';
|
|
||||||
echo '});</script>'."\n";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -646,12 +664,14 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
|
|
||||||
static $last_rang = array();
|
static $last_rang = array();
|
||||||
static $last_parent_section = array();
|
static $last_parent_section = array();
|
||||||
|
static $last_line_order = array();
|
||||||
|
|
||||||
$doc_key = $docType.'_'.$document_id;
|
$doc_key = $docType.'_'.$document_id;
|
||||||
if (!isset(self::$rendered_sections[$doc_key])) {
|
if (!isset(self::$rendered_sections[$doc_key])) {
|
||||||
self::$rendered_sections[$doc_key] = array();
|
self::$rendered_sections[$doc_key] = array();
|
||||||
$last_rang[$doc_key] = 0;
|
$last_rang[$doc_key] = 0;
|
||||||
$last_parent_section[$doc_key] = null;
|
$last_parent_section[$doc_key] = null;
|
||||||
|
$last_line_order[$doc_key] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hole rang dieser Produktzeile
|
// Hole rang dieser Produktzeile
|
||||||
|
|
@ -710,7 +730,8 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
$sql_combined .= " (SELECT MIN(fd.rang) FROM ".MAIN_DB_PREFIX."facture_lines_manager m2";
|
$sql_combined .= " (SELECT MIN(fd.rang) FROM ".MAIN_DB_PREFIX."facture_lines_manager m2";
|
||||||
$sql_combined .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." fd ON fd.rowid = m2.".$tables['fk_line'];
|
$sql_combined .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." fd ON fd.rowid = m2.".$tables['fk_line'];
|
||||||
$sql_combined .= " WHERE m2.parent_section = ".MAIN_DB_PREFIX."facture_lines_manager.rowid";
|
$sql_combined .= " WHERE m2.parent_section = ".MAIN_DB_PREFIX."facture_lines_manager.rowid";
|
||||||
$sql_combined .= " AND m2.document_type = '".$db->escape($docType)."') as first_product_rang";
|
$sql_combined .= " AND m2.document_type = '".$db->escape($docType)."'";
|
||||||
|
$sql_combined .= " AND m2.line_type = 'product') as first_product_rang"; // NUR echte Produkte, keine Subtotals!
|
||||||
$sql_combined .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
$sql_combined .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
$sql_combined .= $this->getDocumentWhere($document_id, $docType, '');
|
$sql_combined .= $this->getDocumentWhere($document_id, $docType, '');
|
||||||
$sql_combined .= " AND (line_type = 'section' OR line_type = 'text')";
|
$sql_combined .= " AND (line_type = 'section' OR line_type = 'text')";
|
||||||
|
|
@ -720,25 +741,113 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
|
|
||||||
while ($obj = $db->fetch_object($resql_combined)) {
|
while ($obj = $db->fetch_object($resql_combined)) {
|
||||||
if ($obj->line_type == 'section') {
|
if ($obj->line_type == 'section') {
|
||||||
// Section nur rendern wenn first_product_rang passt
|
// Section rendern wenn:
|
||||||
if ($obj->first_product_rang > (int)$last_rang[$doc_key] && $obj->first_product_rang <= (int)$current_rang) {
|
// 1. Sie Produkte hat UND first_product_rang im Bereich liegt, ODER
|
||||||
if (!in_array($obj->rowid, self::$rendered_sections[$doc_key])) {
|
// 2. Sie KEINE Produkte hat (leere Section) UND ihre line_order zwischen last_line_order und current_line_order liegt
|
||||||
$section = array(
|
$should_render = false;
|
||||||
'section_id' => $obj->rowid,
|
|
||||||
'title' => $obj->title,
|
|
||||||
'show_subtotal' => $obj->show_subtotal,
|
|
||||||
'collapsed' => $obj->collapsed,
|
|
||||||
'in_facturedet' => $obj->in_facturedet
|
|
||||||
);
|
|
||||||
echo $this->renderSectionHeader($section);
|
|
||||||
self::$rendered_sections[$doc_key][] = $obj->rowid;
|
|
||||||
|
|
||||||
if ($this->debug) {
|
// DEBUG
|
||||||
error_log('[SubtotalTitle] ✅ Section "'.$obj->title.'" gerendert vor rang '.$current_rang);
|
error_log('[SubtotalTitle] DEBUG Section "'.$obj->title.'": line_order='.$obj->line_order.', first_product_rang='.($obj->first_product_rang ?? 'NULL').', last_line_order='.$last_line_order[$doc_key].', last_rang='.$last_rang[$doc_key].', current_rang='.$current_rang);
|
||||||
|
|
||||||
|
if ($obj->first_product_rang !== null) {
|
||||||
|
// Section mit Produkten: prüfe rang-Bereich
|
||||||
|
if ($obj->first_product_rang > (int)$last_rang[$doc_key] && $obj->first_product_rang <= (int)$current_rang) {
|
||||||
|
$should_render = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Leere Section: rendern wenn line_order > last_line_order UND < current_line_order
|
||||||
|
// (SQL filtert bereits nach line_order < current_line_order)
|
||||||
|
if ($obj->line_order > (int)$last_line_order[$doc_key]) {
|
||||||
|
$should_render = true;
|
||||||
|
error_log('[SubtotalTitle] DEBUG → Leere Section "'.$obj->title.'" SOLLTE gerendert werden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($should_render && !in_array($obj->rowid, self::$rendered_sections[$doc_key])) {
|
||||||
|
$section = array(
|
||||||
|
'section_id' => $obj->rowid,
|
||||||
|
'title' => $obj->title,
|
||||||
|
'show_subtotal' => $obj->show_subtotal,
|
||||||
|
'collapsed' => $obj->collapsed,
|
||||||
|
'in_facturedet' => $obj->in_facturedet
|
||||||
|
);
|
||||||
|
echo $this->renderSectionHeader($section);
|
||||||
|
self::$rendered_sections[$doc_key][] = $obj->rowid;
|
||||||
|
|
||||||
|
if ($this->debug) {
|
||||||
|
error_log('[SubtotalTitle] ✅ Section "'.$obj->title.'" gerendert vor rang '.$current_rang.' (leere Section: '.($obj->first_product_rang === null ? 'ja' : 'nein').')');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für LEERE Sections: Subtotal direkt nach Section-Header rendern (wenn show_subtotal aktiviert)
|
||||||
|
if ($obj->first_product_rang === null && $obj->show_subtotal) {
|
||||||
|
$sql_subtotal_empty = "SELECT rowid, title, parent_section, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_subtotal_empty .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
||||||
|
$sql_subtotal_empty .= " AND document_type = '".$db->escape($docType)."'";
|
||||||
|
$sql_subtotal_empty .= " AND parent_section = ".(int)$obj->rowid;
|
||||||
|
$sql_subtotal_empty .= " AND line_type = 'subtotal'";
|
||||||
|
$resql_subtotal_empty = $db->query($sql_subtotal_empty);
|
||||||
|
|
||||||
|
if ($obj_sub_empty = $db->fetch_object($resql_subtotal_empty)) {
|
||||||
|
$subtotal_key_empty = 'subtotal_'.$obj_sub_empty->rowid;
|
||||||
|
if (!in_array($subtotal_key_empty, self::$rendered_sections[$doc_key])) {
|
||||||
|
echo $this->renderSubtotalLine($obj_sub_empty);
|
||||||
|
self::$rendered_sections[$doc_key][] = $subtotal_key_empty;
|
||||||
|
|
||||||
|
if ($this->debug) {
|
||||||
|
error_log('[SubtotalTitle] ✅ Subtotal für leere Section "'.$obj->title.'" gerendert');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} elseif ($obj->line_type == 'text') {
|
} elseif ($obj->line_type == 'text') {
|
||||||
|
// VOR der Textzeile: Prüfe ob es leere Sections gibt, die noch nicht gerendert wurden
|
||||||
|
// und deren line_order kleiner ist als die der Textzeile
|
||||||
|
$sql_empty_sections = "SELECT rowid, title, line_order, show_subtotal, collapsed, in_facturedet,";
|
||||||
|
$sql_empty_sections .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."facture_lines_manager m2";
|
||||||
|
$sql_empty_sections .= " WHERE m2.parent_section = ".MAIN_DB_PREFIX."facture_lines_manager.rowid";
|
||||||
|
$sql_empty_sections .= " AND m2.document_type = '".$db->escape($docType)."'";
|
||||||
|
$sql_empty_sections .= " AND m2.line_type = 'product') as product_count";
|
||||||
|
$sql_empty_sections .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_empty_sections .= $this->getDocumentWhere($document_id, $docType, '');
|
||||||
|
$sql_empty_sections .= " AND line_type = 'section'";
|
||||||
|
$sql_empty_sections .= " AND line_order < ".(int)$obj->line_order;
|
||||||
|
$sql_empty_sections .= " ORDER BY line_order";
|
||||||
|
$resql_empty = $db->query($sql_empty_sections);
|
||||||
|
|
||||||
|
while ($empty_sec = $db->fetch_object($resql_empty)) {
|
||||||
|
// Nur leere Sections (keine Produkte) die noch nicht gerendert wurden
|
||||||
|
if ($empty_sec->product_count == 0 && !in_array($empty_sec->rowid, self::$rendered_sections[$doc_key])) {
|
||||||
|
$section_data = array(
|
||||||
|
'section_id' => $empty_sec->rowid,
|
||||||
|
'title' => $empty_sec->title,
|
||||||
|
'show_subtotal' => $empty_sec->show_subtotal,
|
||||||
|
'collapsed' => $empty_sec->collapsed,
|
||||||
|
'in_facturedet' => $empty_sec->in_facturedet
|
||||||
|
);
|
||||||
|
echo $this->renderSectionHeader($section_data);
|
||||||
|
self::$rendered_sections[$doc_key][] = $empty_sec->rowid;
|
||||||
|
|
||||||
|
if ($this->debug) {
|
||||||
|
error_log('[SubtotalTitle] ✅ Leere Section "'.$empty_sec->title.'" vor Textzeile gerendert');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtotal für diese Section (wenn aktiviert)
|
||||||
|
if ($empty_sec->show_subtotal) {
|
||||||
|
$sql_sub = "SELECT rowid, title, parent_section, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_sub .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
||||||
|
$sql_sub .= " AND document_type = '".$db->escape($docType)."'";
|
||||||
|
$sql_sub .= " AND parent_section = ".(int)$empty_sec->rowid;
|
||||||
|
$sql_sub .= " AND line_type = 'subtotal'";
|
||||||
|
$resql_sub = $db->query($sql_sub);
|
||||||
|
if ($obj_sub = $db->fetch_object($resql_sub)) {
|
||||||
|
echo $this->renderSubtotalLine($obj_sub);
|
||||||
|
self::$rendered_sections[$doc_key][] = 'subtotal_'.$obj_sub->rowid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Textzeile rendern
|
// Textzeile rendern
|
||||||
$text_key = 'text_'.$obj->rowid;
|
$text_key = 'text_'.$obj->rowid;
|
||||||
if (!in_array($text_key, self::$rendered_sections[$doc_key])) {
|
if (!in_array($text_key, self::$rendered_sections[$doc_key])) {
|
||||||
|
|
@ -761,6 +870,7 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
// Merke für nächsten Durchlauf
|
// Merke für nächsten Durchlauf
|
||||||
$last_rang[$doc_key] = $current_rang;
|
$last_rang[$doc_key] = $current_rang;
|
||||||
$last_parent_section[$doc_key] = $current_parent_section;
|
$last_parent_section[$doc_key] = $current_parent_section;
|
||||||
|
$last_line_order[$doc_key] = $current_line_order;
|
||||||
|
|
||||||
// Prüfe ob dies die LETZTE Produktzeile ist - dann Subtotal per JavaScript NACH dieser Zeile einfügen
|
// Prüfe ob dies die LETZTE Produktzeile ist - dann Subtotal per JavaScript NACH dieser Zeile einfügen
|
||||||
if ($current_parent_section) {
|
if ($current_parent_section) {
|
||||||
|
|
@ -808,10 +918,13 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trailing items (leere Sections, Textzeilen) werden jetzt in formAddObjectLine gerendert
|
||||||
|
// da dieser Hook NACH allen Produktzeilen aber VOR dem Eingabeformular aufgerufen wird
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rendert eine Textzeile
|
* Rendert eine Textzeile
|
||||||
*/
|
*/
|
||||||
|
|
@ -928,7 +1041,7 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook: Wird aufgerufen wenn das Produkt-Hinzufügen-Formular gerendert wird
|
* Hook: Wird aufgerufen wenn das Produkt-Hinzufügen-Formular gerendert wird
|
||||||
* Fügt Section-Dropdown hinzu
|
* Fügt Section-Dropdown hinzu UND rendert trailing items (Textzeilen, leere Sections)
|
||||||
*/
|
*/
|
||||||
public function formAddObjectLine($parameters, &$object, &$action, $hookmanager)
|
public function formAddObjectLine($parameters, &$object, &$action, $hookmanager)
|
||||||
{
|
{
|
||||||
|
|
@ -947,7 +1060,112 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
}
|
}
|
||||||
|
|
||||||
$document_id = $object->id;
|
$document_id = $object->id;
|
||||||
|
$tables = DocumentTypeHelper::getTableNames($docType);
|
||||||
|
$doc_key = $docType.'_'.$document_id;
|
||||||
|
|
||||||
|
// Initialisiere rendered_sections falls nicht vorhanden
|
||||||
|
if (!isset(self::$rendered_sections[$doc_key])) {
|
||||||
|
self::$rendered_sections[$doc_key] = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== TRAILING ITEMS RENDERN ==========
|
||||||
|
// Hole ALLE Manager-Einträge, die NACH der letzten facturedet-Zeile kommen
|
||||||
|
// und noch nicht gerendert wurden
|
||||||
|
|
||||||
|
// Finde die höchste line_order einer Zeile, die in facturedet ist
|
||||||
|
$sql_max_rendered = "SELECT MAX(m.line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
|
||||||
|
$sql_max_rendered .= " WHERE m.".$tables['fk_parent']." = ".(int)$document_id;
|
||||||
|
$sql_max_rendered .= " AND m.document_type = '".$db->escape($docType)."'";
|
||||||
|
$sql_max_rendered .= " AND m.".$tables['fk_line']." IS NOT NULL";
|
||||||
|
$res_max = $db->query($sql_max_rendered);
|
||||||
|
$obj_max = $db->fetch_object($res_max);
|
||||||
|
$last_rendered_order = $obj_max && $obj_max->max_order ? (int)$obj_max->max_order : 0;
|
||||||
|
|
||||||
|
if ($this->debug) {
|
||||||
|
error_log('[SubtotalTitle] formAddObjectLine: Letzte gerenderte line_order = '.$last_rendered_order);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hole alle trailing items (alles mit line_order > last_rendered_order)
|
||||||
|
$sql_trailing = "SELECT rowid, line_type, title, line_order, parent_section, show_subtotal, collapsed, in_facturedet";
|
||||||
|
$sql_trailing .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_trailing .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
||||||
|
$sql_trailing .= " AND document_type = '".$db->escape($docType)."'";
|
||||||
|
$sql_trailing .= " AND line_order > ".$last_rendered_order;
|
||||||
|
$sql_trailing .= " ORDER BY line_order";
|
||||||
|
$resql_trailing = $db->query($sql_trailing);
|
||||||
|
|
||||||
|
while ($trailing = $db->fetch_object($resql_trailing)) {
|
||||||
|
if ($trailing->line_type == 'section') {
|
||||||
|
// Section-Header rendern (falls noch nicht gerendert)
|
||||||
|
if (!in_array($trailing->rowid, self::$rendered_sections[$doc_key])) {
|
||||||
|
$section_data = array(
|
||||||
|
'section_id' => $trailing->rowid,
|
||||||
|
'title' => $trailing->title,
|
||||||
|
'show_subtotal' => $trailing->show_subtotal,
|
||||||
|
'collapsed' => $trailing->collapsed,
|
||||||
|
'in_facturedet' => $trailing->in_facturedet
|
||||||
|
);
|
||||||
|
echo $this->renderSectionHeader($section_data);
|
||||||
|
self::$rendered_sections[$doc_key][] = $trailing->rowid;
|
||||||
|
|
||||||
|
if ($this->debug) {
|
||||||
|
error_log('[SubtotalTitle] ✅ Trailing Section "'.$trailing->title.'" gerendert (order='.$trailing->line_order.')');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls Subtotal aktiviert, auch Subtotal rendern
|
||||||
|
if ($trailing->show_subtotal) {
|
||||||
|
$sql_sub = "SELECT rowid, title, parent_section, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_sub .= " WHERE parent_section = ".(int)$trailing->rowid;
|
||||||
|
$sql_sub .= " AND line_type = 'subtotal'";
|
||||||
|
$sql_sub .= " AND document_type = '".$db->escape($docType)."'";
|
||||||
|
$resql_sub = $db->query($sql_sub);
|
||||||
|
|
||||||
|
if ($obj_sub = $db->fetch_object($resql_sub)) {
|
||||||
|
$subtotal_key = 'subtotal_'.$obj_sub->rowid;
|
||||||
|
if (!in_array($subtotal_key, self::$rendered_sections[$doc_key])) {
|
||||||
|
echo $this->renderSubtotalLine($obj_sub);
|
||||||
|
self::$rendered_sections[$doc_key][] = $subtotal_key;
|
||||||
|
|
||||||
|
if ($this->debug) {
|
||||||
|
error_log('[SubtotalTitle] ✅ Trailing Subtotal für Section "'.$trailing->title.'" gerendert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($trailing->line_type == 'text') {
|
||||||
|
// Textzeile rendern
|
||||||
|
$text_key = 'text_'.$trailing->rowid;
|
||||||
|
if (!in_array($text_key, self::$rendered_sections[$doc_key])) {
|
||||||
|
$textline = array(
|
||||||
|
'id' => $trailing->rowid,
|
||||||
|
'title' => $trailing->title,
|
||||||
|
'parent_section' => $trailing->parent_section,
|
||||||
|
'line_order' => $trailing->line_order,
|
||||||
|
'in_facturedet' => $trailing->in_facturedet
|
||||||
|
);
|
||||||
|
echo $this->renderTextLine($textline);
|
||||||
|
self::$rendered_sections[$doc_key][] = $text_key;
|
||||||
|
|
||||||
|
if ($this->debug) {
|
||||||
|
error_log('[SubtotalTitle] ✅ Trailing Textzeile "'.$trailing->title.'" gerendert (order='.$trailing->line_order.')');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($trailing->line_type == 'subtotal') {
|
||||||
|
// Subtotal rendern (falls nicht schon mit Section gerendert)
|
||||||
|
$subtotal_key = 'subtotal_'.$trailing->rowid;
|
||||||
|
if (!in_array($subtotal_key, self::$rendered_sections[$doc_key])) {
|
||||||
|
echo $this->renderSubtotalLine($trailing);
|
||||||
|
self::$rendered_sections[$doc_key][] = $subtotal_key;
|
||||||
|
|
||||||
|
if ($this->debug) {
|
||||||
|
error_log('[SubtotalTitle] ✅ Trailing Subtotal gerendert (order='.$trailing->line_order.')');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dann das Section-Dropdown rendern
|
||||||
echo $this->renderSectionDropdown($document_id, $docType);
|
echo $this->renderSectionDropdown($document_id, $docType);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
|
|
@ -254,8 +254,45 @@ if (typeof SubtotalTitleLoaded === 'undefined') {
|
||||||
*/
|
*/
|
||||||
function getDocumentInfo() {
|
function getDocumentInfo() {
|
||||||
var url = window.location.href;
|
var url = window.location.href;
|
||||||
|
|
||||||
|
// Versuche ID aus URL-Parametern zu extrahieren (mehrere Formate)
|
||||||
|
var id = null;
|
||||||
|
|
||||||
|
// Format 1: ?id=123 oder &id=123
|
||||||
var match = url.match(/[?&]id=(\d+)/);
|
var match = url.match(/[?&]id=(\d+)/);
|
||||||
var id = match ? match[1] : null;
|
if (match) {
|
||||||
|
id = match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format 2: Aus URLSearchParams (robuster)
|
||||||
|
if (!id) {
|
||||||
|
try {
|
||||||
|
var params = new URLSearchParams(window.location.search);
|
||||||
|
id = params.get('id');
|
||||||
|
} catch(e) {
|
||||||
|
// URLSearchParams nicht verfügbar in alten Browsern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format 3: Aus dem DOM (falls ID dort gespeichert ist)
|
||||||
|
if (!id) {
|
||||||
|
// Versuche aus versteckten Feldern oder data-Attributen
|
||||||
|
var $idInput = $('input[name="id"], input[name="facid"], input[name="socid"]').first();
|
||||||
|
if ($idInput.length > 0) {
|
||||||
|
id = $idInput.val();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format 4: Aus der Seite selbst (Dolibarr zeigt oft die ID an)
|
||||||
|
if (!id) {
|
||||||
|
var $refDiv = $('.refid, .ref').first();
|
||||||
|
if ($refDiv.length > 0) {
|
||||||
|
var refMatch = $refDiv.text().match(/\((\d+)\)/);
|
||||||
|
if (refMatch) {
|
||||||
|
id = refMatch[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Erkenne Dokumenttyp anhand der URL
|
// Erkenne Dokumenttyp anhand der URL
|
||||||
var docType = 'invoice'; // Default
|
var docType = 'invoice'; // Default
|
||||||
|
|
@ -265,6 +302,8 @@ function getDocumentInfo() {
|
||||||
docType = 'order';
|
docType = 'order';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugLog('getDocumentInfo: id=' + id + ', type=' + docType + ', url=' + url);
|
||||||
|
|
||||||
return { id: id, type: docType };
|
return { id: id, type: docType };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -332,7 +371,13 @@ function moveSection(sectionId, direction) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
window.location.href = window.location.pathname + window.location.search;
|
window.location.href = window.location.pathname + window.location.search;
|
||||||
} else {
|
} else {
|
||||||
alert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
|
// "Already at top/bottom" ist kein echter Fehler, sondern eine Info
|
||||||
|
if (response.error === 'Already at top' || response.error === 'Already at bottom') {
|
||||||
|
debugLog('Section ist bereits am ' + (response.error === 'Already at top' ? 'Anfang' : 'Ende'));
|
||||||
|
// Kein Alert nötig - einfach nichts tun
|
||||||
|
} else {
|
||||||
|
alert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 'json').fail(function(xhr, status, error) {
|
}, 'json').fail(function(xhr, status, error) {
|
||||||
if (SUBTOTAL_DEBUG) {
|
if (SUBTOTAL_DEBUG) {
|
||||||
|
|
@ -788,8 +833,13 @@ function addUnlinkColumn() {
|
||||||
if (rowId.match(/^row-\d+/)) {
|
if (rowId.match(/^row-\d+/)) {
|
||||||
var lineId = rowId.replace('row-', '').split('-')[0];
|
var lineId = rowId.replace('row-', '').split('-')[0];
|
||||||
var parentSection = $row.attr('data-parent-section');
|
var parentSection = $row.attr('data-parent-section');
|
||||||
|
|
||||||
if (parentSection && parentSection !== 'null') {
|
// Robustere Prüfung: Hat gültige Section-ID?
|
||||||
|
var hasValidSection = parentSection && parentSection !== 'null' && parentSection !== 'undefined' && parentSection !== '' && parseInt(parentSection) > 0;
|
||||||
|
|
||||||
|
debugLog('🔗 Zeile ' + lineId + ': parent_section="' + parentSection + '" → hasValidSection=' + hasValidSection);
|
||||||
|
|
||||||
|
if (hasValidSection) {
|
||||||
// Hat Section → Unlink-Button
|
// Hat Section → Unlink-Button
|
||||||
$row.append('<td class="linecolunlink center"><a href="#" onclick="removeFromSection(' + lineId + '); return false;" title="Aus Positionsgruppe entfernen"><span class="fas fa-unlink" style="color:#888;"></span></a></td>');
|
$row.append('<td class="linecolunlink center"><a href="#" onclick="removeFromSection(' + lineId + '); return false;" title="Aus Positionsgruppe entfernen"><span class="fas fa-unlink" style="color:#888;"></span></a></td>');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -811,9 +861,10 @@ function addUnlinkColumn() {
|
||||||
* PLAN B: Section-Assignment beim Produkt hinzufügen
|
* PLAN B: Section-Assignment beim Produkt hinzufügen
|
||||||
*/
|
*/
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
// Unlink-Spalte hinzufügen (nach data-Attributen)
|
// Unlink-Spalte hinzufügen - NACH den PHP-Scripts die data-parent-section setzen
|
||||||
setTimeout(addUnlinkColumn, 600);
|
// Timeout erhöht auf 1500ms um sicherzustellen dass alle Attribute gesetzt sind
|
||||||
|
setTimeout(addUnlinkColumn, 1500);
|
||||||
|
|
||||||
// 1. Beim Submit des Formulars: Section merken
|
// 1. Beim Submit des Formulars: Section merken
|
||||||
$(document).on('submit', 'form[name="addproduct"]', function(e) {
|
$(document).on('submit', 'form[name="addproduct"]', function(e) {
|
||||||
|
|
@ -847,6 +898,7 @@ $(document).ready(function() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Weist das neueste Produkt einer Section zu
|
* Weist das neueste Produkt einer Section zu
|
||||||
|
* Nach der Zuweisung wird die Darstellung per JavaScript aktualisiert (KEIN zweiter Reload)
|
||||||
*/
|
*/
|
||||||
function assignLastProductToSection(sectionId, factureId) {
|
function assignLastProductToSection(sectionId, factureId) {
|
||||||
var docInfo = getDocumentInfo();
|
var docInfo = getDocumentInfo();
|
||||||
|
|
@ -860,7 +912,35 @@ function assignLastProductToSection(sectionId, factureId) {
|
||||||
debugLog('✅ Assignment Response: ' + JSON.stringify(response));
|
debugLog('✅ Assignment Response: ' + JSON.stringify(response));
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
debugLog('✅ Produkt #' + response.product_id + ' zu Section zugewiesen');
|
debugLog('✅ Produkt #' + response.product_id + ' zu Section zugewiesen');
|
||||||
window.location.href = window.location.pathname + window.location.search;
|
|
||||||
|
// KEIN zweiter Reload - stattdessen Zeile per JavaScript aktualisieren
|
||||||
|
var $productRow = $('#row-' + response.product_id);
|
||||||
|
if ($productRow.length > 0) {
|
||||||
|
// Setze data-parent-section Attribut
|
||||||
|
$productRow.attr('data-parent-section', sectionId);
|
||||||
|
|
||||||
|
// Verschiebe die Zeile an die richtige Position (nach der Section)
|
||||||
|
var $sectionRow = $('tr.section-header[data-section-id="' + sectionId + '"]');
|
||||||
|
if ($sectionRow.length > 0) {
|
||||||
|
// Finde das letzte Element dieser Section
|
||||||
|
var $lastInSection = $('tr[data-parent-section="' + sectionId + '"]').last();
|
||||||
|
if ($lastInSection.length > 0 && $lastInSection[0] !== $productRow[0]) {
|
||||||
|
$lastInSection.after($productRow);
|
||||||
|
} else if ($lastInSection.length === 0) {
|
||||||
|
// Erstes Produkt in der Section
|
||||||
|
$sectionRow.after($productRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Färbung aktualisieren
|
||||||
|
colorSections();
|
||||||
|
|
||||||
|
debugLog('✅ Zeile per JavaScript aktualisiert - kein Reload nötig');
|
||||||
|
} else {
|
||||||
|
// Fallback: Wenn Zeile nicht gefunden, doch reloaden
|
||||||
|
debugLog('⚠️ Zeile nicht gefunden, Fallback: Reload');
|
||||||
|
window.location.href = window.location.pathname + window.location.search;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
debugLog('❌ Fehler: ' + response.error);
|
debugLog('❌ Fehler: ' + response.error);
|
||||||
}
|
}
|
||||||
|
|
@ -1669,7 +1749,8 @@ function linkToNearestSection(lineId) {
|
||||||
debugLog(' Response: ' + JSON.stringify(response));
|
debugLog(' Response: ' + JSON.stringify(response));
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
window.location.reload();
|
// safeReload statt reload() um POST-Warnung zu vermeiden
|
||||||
|
safeReload();
|
||||||
} else {
|
} else {
|
||||||
showErrorAlert('Fehler: ' + (response.error || 'Unbekannter Fehler'));
|
showErrorAlert('Fehler: ' + (response.error || 'Unbekannter Fehler'));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
sql/llx_facture_lines_manager.sql
Normal file
51
sql/llx_facture_lines_manager.sql
Normal 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;
|
||||||
Loading…
Reference in a new issue