Compare commits
No commits in common. "main" and "v3.5-facture" have entirely different histories.
main
...
v3.5-factu
40 changed files with 1142 additions and 4968 deletions
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(grep:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
2
.kdev4/subtotaltitle.kdev4
Executable file
2
.kdev4/subtotaltitle.kdev4
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
[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)
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
# 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
Executable file → Normal file
241
README.md
Executable file → Normal file
|
|
@ -1,205 +1,88 @@
|
|||
# SubtotalTitle - Dolibarr Modul
|
||||
# SubtotalTitle - Facturedet Sync Update
|
||||
|
||||
Erweitert Rechnungen, Angebote und Kundenaufträge um **Sections**, **Textzeilen** und **Zwischensummen**.
|
||||
## 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
|
||||
|
||||
---
|
||||
## Installation
|
||||
|
||||
## 🔑 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
|
||||
### 1. Dateien kopieren
|
||||
|
||||
```
|
||||
[!-- IF {object_has_sections} --]
|
||||
Dieses Dokument enthält {object_count_sections} Section(s).
|
||||
[!-- ENDIF {object_has_sections} --]
|
||||
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
|
||||
|
||||
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_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} --]
|
||||
[!-- IF {line_is_normal} --]
|
||||
{line_pos} {line_qty} {line_desc} {line_up_locale} € {line_price_ht_locale} €
|
||||
[!-- ENDIF {line_is_free_line} --]
|
||||
[!-- ENDIF {line_is_normal} --]
|
||||
|
||||
[!-- IF {line_is_subtotal} --]
|
||||
───────────────────────────────────────
|
||||
Zwischensumme: {line_price_ht_locale} €
|
||||
───────────────────────────────────────
|
||||
[!-- ENDIF {line_is_subtotal} --]
|
||||
|
||||
[!-- END row.lines --]
|
||||
```
|
||||
|
||||
---
|
||||
## special_code Werte
|
||||
|
||||
## 📋 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.
|
||||
| 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}` |
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
<?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]);
|
||||
|
|
@ -2,31 +2,22 @@
|
|||
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 . ', docType=' . $docType);
|
||||
subtotaltitle_debug_log('🔍 assign_last_product: facture=' . $facture_id . ', section=' . $section_id);
|
||||
|
||||
if (!$facture_id || !$section_id || !$docType) {
|
||||
if (!$facture_id || !$section_id) {
|
||||
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 dieses Dokuments (höchster rang)
|
||||
$sql = "SELECT rowid, rang FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
|
||||
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
|
||||
// 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;
|
||||
$sql .= " ORDER BY rang DESC LIMIT 1";
|
||||
$resql = $db->query($sql);
|
||||
|
||||
|
|
@ -41,67 +32,37 @@ $product_id = $product->rowid;
|
|||
|
||||
subtotaltitle_debug_log(' → Neustes Produkt: #' . $product_id . ' (rang=' . $product->rang . ')');
|
||||
|
||||
// Prüfe ob schon in Manager-Tabelle (anhand der Detail-FK-Spalte)
|
||||
// Prüfe ob schon in Manager-Tabelle
|
||||
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql .= " WHERE ".$tables['fk_line']." = ".(int)$product_id;
|
||||
$sql .= " WHERE fk_facturedet = ".(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
|
||||
// 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';
|
||||
$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);
|
||||
}
|
||||
|
||||
$sql_ins = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$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())";
|
||||
$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())";
|
||||
$db->query($sql_ins);
|
||||
|
||||
subtotaltitle_debug_log(' → Produkt zu Manager-Tabelle hinzugefügt (line_order=' . $new_line_order . ')');
|
||||
subtotaltitle_debug_log(' → Produkt zu Manager-Tabelle hinzugefügt (line_order=' . $next_order . ')');
|
||||
} else {
|
||||
// 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);
|
||||
// Produkt existiert - UPDATE parent_section
|
||||
subtotaltitle_debug_log('🔵🔵🔵 assign_last_product: Produkt #'.$product_id.' → parent_section='.$section_id);
|
||||
|
||||
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql_upd .= " SET parent_section = ".(int)$section_id;
|
||||
$sql_upd .= ", line_order = ".$new_line_order;
|
||||
$sql_upd .= " WHERE ".$tables['fk_line']." = ".(int)$product_id;
|
||||
$sql_upd .= " WHERE fk_facturedet = ".(int)$product_id;
|
||||
$db->query($sql_upd);
|
||||
|
||||
subtotaltitle_debug_log(' → parent_section und line_order updated');
|
||||
subtotaltitle_debug_log(' → parent_section updated');
|
||||
}
|
||||
|
||||
// Neu sortieren
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
<?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
|
||||
]);
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
<?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
|
||||
]);
|
||||
|
|
@ -1,49 +1,30 @@
|
|||
<?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 || !$docType) {
|
||||
if (!$facture_id || !$title) {
|
||||
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 ".$tables['fk_parent']." = ".(int)$facture_id;
|
||||
$sql .= " AND document_type = '".$db->escape($docType)."'";
|
||||
$sql .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$resql = $db->query($sql);
|
||||
$obj = $db->fetch_object($resql);
|
||||
$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';
|
||||
$next_order = ($obj->max_order ? $obj->max_order + 1 : 1);
|
||||
|
||||
// Erstelle Section
|
||||
$sql = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$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())";
|
||||
$sql .= " (fk_facture, line_type, title, line_order, date_creation)";
|
||||
$sql .= " VALUES (".(int)$facture_id.", 'section', '".$db->escape($title)."', ".$next_order.", NOW())";
|
||||
|
||||
if ($db->query($sql)) {
|
||||
$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]);
|
||||
echo json_encode(['success' => true, 'section_id' => $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager")]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => $db->lasterror()]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,42 +6,27 @@ 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 || !$docType) {
|
||||
if (!$facture_id || !$text) {
|
||||
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 ".$tables['fk_parent']." = ".(int)$facture_id;
|
||||
$sql .= " AND document_type = '".$db->escape($docType)."'";
|
||||
$sql .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$resql = $db->query($sql);
|
||||
$obj = $db->fetch_object($resql);
|
||||
$next_order = ($obj->max_order ? $obj->max_order + 1 : 1);
|
||||
|
||||
// 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';
|
||||
|
||||
// Füge Textzeile ein
|
||||
$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 .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'text', '".$db->escape($text)."', ".$next_order.", 0, NOW())";
|
||||
$sql .= " (fk_facture, line_type, title, line_order, date_creation)";
|
||||
$sql .= " VALUES (".(int)$facture_id.", 'text', '".$db->escape($text)."', ".$next_order.", NOW())";
|
||||
|
||||
if ($db->query($sql)) {
|
||||
$new_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");
|
||||
|
|
|
|||
|
|
@ -1,31 +1,23 @@
|
|||
<?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 . ', docType=' . $docType);
|
||||
subtotaltitle_debug_log('🔄 delete_section: section=' . $section_id . ', force=' . $force);
|
||||
|
||||
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 ".$tables['fk_parent']." as doc_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql = "SELECT fk_facture FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql .= " WHERE rowid = ".(int)$section_id;
|
||||
$sql .= " AND line_type = 'section'";
|
||||
$resql = $db->query($sql);
|
||||
|
|
@ -36,61 +28,55 @@ if (!$resql || $db->num_rows($resql) == 0) {
|
|||
}
|
||||
|
||||
$section = $db->fetch_object($resql);
|
||||
$document_id = $section->doc_id;
|
||||
$facture_id = $section->fk_facture;
|
||||
|
||||
// 2. Pruefe Dokumentstatus
|
||||
$object = DocumentTypeHelper::loadDocument($docType, $document_id, $db);
|
||||
if (!$object) {
|
||||
echo json_encode(['success' => false, 'error' => 'Dokument nicht gefunden']);
|
||||
exit;
|
||||
}
|
||||
// 2. Prüfe Rechnungsstatus
|
||||
$facture = new Facture($db);
|
||||
$facture->fetch($facture_id);
|
||||
|
||||
$isDraft = DocumentTypeHelper::isDraft($object, $docType);
|
||||
if ($force && !$isDraft) {
|
||||
echo json_encode(['success' => false, 'error' => 'Dokument ist nicht im Entwurf']);
|
||||
if ($force && $facture->statut != Facture::STATUS_DRAFT) {
|
||||
echo json_encode(['success' => false, 'error' => 'Rechnung ist nicht im Entwurf']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. Hole Produkt-IDs DIREKT aus DB
|
||||
$product_ids = [];
|
||||
$sql_products = "SELECT ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql_products = "SELECT fk_facturedet 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)) {
|
||||
if ($prod->detail_id) {
|
||||
$product_ids[] = (int)$prod->detail_id;
|
||||
}
|
||||
$product_ids[] = (int)$prod->fk_facturedet;
|
||||
}
|
||||
|
||||
$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 Dokument loeschen
|
||||
// 4. Force-Delete: Produkte aus Rechnung löschen
|
||||
if ($force && $product_count > 0) {
|
||||
subtotaltitle_debug_log('Loesche ' . $product_count . ' Zeilen aus Dokument...');
|
||||
subtotaltitle_debug_log('🗑️ Lösche ' . $product_count . ' Zeilen aus Rechnung...');
|
||||
|
||||
foreach ($product_ids as $line_id) {
|
||||
$sql_del_line = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".(int)$line_id;
|
||||
$sql_del_line = "DELETE FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid = ".(int)$line_id;
|
||||
$res_del = $db->query($sql_del_line);
|
||||
|
||||
if ($res_del) {
|
||||
subtotaltitle_debug_log('Detail geloescht: ' . $line_id);
|
||||
subtotaltitle_debug_log('✅ facturedet gelöscht: ' . $line_id);
|
||||
} else {
|
||||
subtotaltitle_debug_log('SQL Fehler: ' . $line_id . ' - ' . $db->lasterror());
|
||||
subtotaltitle_debug_log('❌ SQL Fehler: ' . $line_id . ' - ' . $db->lasterror());
|
||||
}
|
||||
}
|
||||
|
||||
// Aus Manager-Tabelle loeschen
|
||||
// Aus Manager-Tabelle löschen
|
||||
$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
|
||||
|
|
@ -100,34 +86,35 @@ if ($force && $product_count > 0) {
|
|||
$sql .= " AND line_type = 'product'";
|
||||
$db->query($sql);
|
||||
|
||||
subtotaltitle_debug_log($product_count . ' Produkte freigegeben');
|
||||
subtotaltitle_debug_log('🔓 ' . $product_count . ' Produkte freigegeben');
|
||||
}
|
||||
|
||||
// ========== SUBTOTAL LOESCHEN ==========
|
||||
$sql_subtotal = "SELECT rowid, ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
// ========== NEU: SUBTOTAL LÖSCHEN ==========
|
||||
// Hole Subtotal dieser Section (falls vorhanden)
|
||||
$sql_subtotal = "SELECT rowid, fk_facturedet 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 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;
|
||||
// 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;
|
||||
$db->query($sql_del_fd);
|
||||
subtotaltitle_debug_log('Subtotal aus Detail geloescht: ' . $obj_sub->detail_id);
|
||||
subtotaltitle_debug_log('✅ Subtotal aus facturedet gelöscht: ' . $obj_sub->fk_facturedet);
|
||||
}
|
||||
|
||||
// Aus Manager-Tabelle loeschen
|
||||
// Aus Manager-Tabelle löschen
|
||||
$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 geloescht: ' . $obj_sub->rowid);
|
||||
subtotaltitle_debug_log('✅ Subtotal aus Manager gelöscht: ' . $obj_sub->rowid);
|
||||
}
|
||||
|
||||
// ========== VERWAISTE SUBTOTALS AUFRAEUMEN ==========
|
||||
$sql_orphans = "SELECT s.rowid, s.".$tables['fk_line']." as detail_id, s.parent_section
|
||||
// ========== 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
|
||||
FROM ".MAIN_DB_PREFIX."facture_lines_manager s
|
||||
WHERE s.".$tables['fk_parent']." = ".(int)$document_id."
|
||||
AND s.document_type = '".$db->escape($docType)."'
|
||||
WHERE s.fk_facture = ".(int)$facture_id."
|
||||
AND s.line_type = 'subtotal'
|
||||
AND s.parent_section IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
|
|
@ -139,33 +126,36 @@ $res_orphans = $db->query($sql_orphans);
|
|||
|
||||
$orphan_count = 0;
|
||||
while ($orphan = $db->fetch_object($res_orphans)) {
|
||||
if ($orphan->detail_id > 0) {
|
||||
$sql_del_orphan_fd = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".(int)$orphan->detail_id;
|
||||
// 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;
|
||||
$db->query($sql_del_orphan_fd);
|
||||
subtotaltitle_debug_log('Verwaistes Subtotal aus Detail geloescht: ' . $orphan->detail_id);
|
||||
subtotaltitle_debug_log('🧹 Verwaistes Subtotal aus facturedet gelöscht: ' . $orphan->fk_facturedet . ' (parent_section=' . $orphan->parent_section . ')');
|
||||
}
|
||||
|
||||
// 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('Aufgeraeumt: ' . $orphan_count . ' verwaiste Subtotals entfernt');
|
||||
subtotaltitle_debug_log('🧹 Aufgeräumt: ' . $orphan_count . ' verwaiste Subtotals entfernt');
|
||||
}
|
||||
// ========== ENDE VERWAISTE SUBTOTALS ==========
|
||||
|
||||
// 5. Section selbst loeschen
|
||||
// 5. Section selbst löschen
|
||||
$sql = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql .= " WHERE rowid = ".(int)$section_id;
|
||||
$db->query($sql);
|
||||
|
||||
// Dokumenttotale neu berechnen
|
||||
$object->update_price(1);
|
||||
// Rechnungstotale neu berechnen (nach allen Löschungen)
|
||||
$facture->update_price(1);
|
||||
|
||||
// 6. Neuordnen
|
||||
$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 .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$sql .= " ORDER BY line_order";
|
||||
$resql = $db->query($sql);
|
||||
|
||||
|
|
@ -179,23 +169,20 @@ while ($obj = $db->fetch_object($resql)) {
|
|||
}
|
||||
|
||||
// 7. Sync rang
|
||||
$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 = "SELECT fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$sql .= " AND line_type = 'product'";
|
||||
$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'];
|
||||
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facturedet";
|
||||
$sql_upd .= " SET rang = ".$rang;
|
||||
$sql_upd .= " WHERE rowid = ".(int)$obj->detail_id;
|
||||
$sql_upd .= " WHERE rowid = ".(int)$obj->fk_facturedet;
|
||||
$db->query($sql_upd);
|
||||
$rang++;
|
||||
}
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
|
|
|
|||
|
|
@ -6,42 +6,20 @@ 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');
|
||||
|
||||
// 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);
|
||||
subtotaltitle_debug_log('🗑️ delete_textline: id=' . $textline_id);
|
||||
|
||||
if (!$textline_id) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// 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";
|
||||
// 1. Hole facture_id BEVOR wir löschen
|
||||
$sql = "SELECT fk_facture FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql .= " WHERE rowid = ".(int)$textline_id;
|
||||
$resql = $db->query($sql);
|
||||
|
||||
|
|
@ -51,54 +29,31 @@ if (!$resql || $db->num_rows($resql) == 0) {
|
|||
}
|
||||
|
||||
$obj = $db->fetch_object($resql);
|
||||
$document_id = $obj->doc_id;
|
||||
$facture_id = $obj->fk_facture;
|
||||
|
||||
$db->begin();
|
||||
|
||||
// 2. DELETE ausfuehren
|
||||
// 2. DELETE ausführen
|
||||
$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. 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";
|
||||
// 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";
|
||||
$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++;
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
|
@ -6,44 +6,26 @@ 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 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";
|
||||
// Hole erst fk_facturedet (falls Textzeile in Rechnung ist)
|
||||
$sql_get = "SELECT fk_facturedet 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);
|
||||
|
||||
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;
|
||||
}
|
||||
$fk_facturedet = $obj ? $obj->fk_facturedet : null;
|
||||
|
||||
// Update Manager-Tabelle
|
||||
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
|
|
@ -56,15 +38,15 @@ if (!$db->query($sql)) {
|
|||
exit;
|
||||
}
|
||||
|
||||
// Falls in Detail-Tabelle vorhanden, dort auch updaten
|
||||
if ($fk_detail > 0 && $tables) {
|
||||
$sql_fd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
|
||||
// Falls in facturedet vorhanden, dort auch updaten
|
||||
if ($fk_facturedet > 0) {
|
||||
$sql_fd = "UPDATE ".MAIN_DB_PREFIX."facturedet";
|
||||
$sql_fd .= " SET description = '".$db->escape($text)."'";
|
||||
$sql_fd .= " WHERE rowid = ".(int)$fk_detail;
|
||||
$sql_fd .= " WHERE rowid = ".(int)$fk_facturedet;
|
||||
$db->query($sql_fd);
|
||||
subtotaltitle_debug_log('Textzeile + Detail geaendert (docType='.$docType.')');
|
||||
subtotaltitle_debug_log('✅ Textzeile + facturedet geändert');
|
||||
} else {
|
||||
subtotaltitle_debug_log('Textzeile geaendert (nicht in Detail-Tabelle)');
|
||||
subtotaltitle_debug_log('✅ Textzeile geändert (nicht in facturedet)');
|
||||
}
|
||||
|
||||
echo json_encode(array('success' => true, 'synced_detail' => ($fk_detail > 0)));
|
||||
echo json_encode(array('success' => true, 'synced_facturedet' => ($fk_facturedet > 0)));
|
||||
|
|
|
|||
|
|
@ -1,182 +0,0 @@
|
|||
<?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 " → 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 " → 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 " → 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>";
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<?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'
|
||||
]);
|
||||
|
|
@ -2,30 +2,20 @@
|
|||
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');
|
||||
subtotaltitle_debug_log('🔄 get_sections: facture=' . $facture_id . ', docType=' . $docType);
|
||||
subtotaltitle_debug_log('🔄 get_sections: facture=' . $facture_id);
|
||||
|
||||
if (!$facture_id || !$docType) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
|
||||
if (!$facture_id) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Missing facture_id'));
|
||||
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 ALLE Sections für diesen Dokumenttyp
|
||||
// Hole ALLE Sections
|
||||
$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 AND p.line_type = 'product' AND p.document_type = '".$db->escape($docType)."') as product_count";
|
||||
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."facture_lines_manager p WHERE p.parent_section = s.rowid) as product_count";
|
||||
$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 .= " WHERE s.fk_facture = ".(int)$facture_id;
|
||||
$sql .= " AND s.line_type = 'section'";
|
||||
$sql .= " ORDER BY s.line_order";
|
||||
$resql = $db->query($sql);
|
||||
|
|
|
|||
|
|
@ -6,28 +6,17 @@ 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 || !$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'));
|
||||
if (!$facture_id) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Missing facture_id'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = "SELECT rowid, title, line_order, parent_section FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
|
||||
$sql .= " AND document_type = '".$db->escape($docType)."'";
|
||||
$sql .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$sql .= " AND line_type = 'text'";
|
||||
$sql .= " ORDER BY line_order";
|
||||
$resql = $db->query($sql);
|
||||
|
|
|
|||
|
|
@ -1,565 +0,0 @@
|
|||
<?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'));
|
||||
}
|
||||
|
|
@ -2,40 +2,28 @@
|
|||
define('NOTOKENRENEWAL', 1);
|
||||
require '../../../main.inc.php';
|
||||
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
|
||||
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
|
||||
|
||||
$line_ids_json = GETPOST('line_ids', 'alpha');
|
||||
$document_id = GETPOST('facture_id', 'int'); // Kompatibilitaet: facture_id wird auch fuer andere Typen verwendet
|
||||
$docType = GETPOST('document_type', 'alpha');
|
||||
$facture_id = GETPOST('facture_id', 'int');
|
||||
|
||||
$line_ids = json_decode($line_ids_json, true);
|
||||
|
||||
if (!is_array($line_ids) || count($line_ids) == 0 || !$document_id) {
|
||||
if (!is_array($line_ids) || count($line_ids) == 0 || !$facture_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']);
|
||||
// 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']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 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);
|
||||
subtotaltitle_debug_log('🗑️ Massenlöschung: ' . count($line_ids) . ' Zeilen');
|
||||
|
||||
$db->begin();
|
||||
|
||||
|
|
@ -43,25 +31,24 @@ $deleted = 0;
|
|||
foreach ($line_ids as $line_id) {
|
||||
$line_id = (int)$line_id;
|
||||
|
||||
// Aus Detail-Tabelle loeschen
|
||||
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".$line_id;
|
||||
// Aus facturedet löschen
|
||||
$sql = "DELETE FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid = ".$line_id;
|
||||
if ($db->query($sql)) {
|
||||
$deleted++;
|
||||
subtotaltitle_debug_log('Zeile geloescht: ' . $line_id);
|
||||
subtotaltitle_debug_log('✅ Zeile gelöscht: ' . $line_id);
|
||||
}
|
||||
|
||||
// Aus Manager-Tabelle loeschen
|
||||
$sql_manager = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE ".$tables['fk_line']." = ".$line_id;
|
||||
// Aus Manager-Tabelle löschen
|
||||
$sql_manager = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE fk_facturedet = ".$line_id;
|
||||
$db->query($sql_manager);
|
||||
}
|
||||
|
||||
// Summen neu berechnen
|
||||
$object->update_price(1);
|
||||
$facture->update_price(1);
|
||||
|
||||
// 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 .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$sql .= " ORDER BY line_order";
|
||||
$resql = $db->query($sql);
|
||||
|
||||
|
|
@ -73,24 +60,21 @@ while ($obj = $db->fetch_object($resql)) {
|
|||
}
|
||||
|
||||
// rang 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 = "SELECT fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$sql .= " AND line_type = 'product'";
|
||||
$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;
|
||||
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facturedet SET rang = ".$rang." WHERE rowid = ".(int)$obj->fk_facturedet;
|
||||
$db->query($sql_upd);
|
||||
$rang++;
|
||||
}
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
subtotaltitle_debug_log('Massenloeschung abgeschlossen: ' . $deleted . ' von ' . count($line_ids));
|
||||
subtotaltitle_debug_log('🗑️ Massenlöschung abgeschlossen: ' . $deleted . ' von ' . count($line_ids));
|
||||
|
||||
echo json_encode(['success' => true, 'deleted' => $deleted]);
|
||||
|
|
@ -1,41 +1,19 @@
|
|||
<?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 ".$tables['fk_parent']." as doc_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql = "SELECT fk_facture 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) {
|
||||
|
|
@ -44,14 +22,13 @@ if (!$resql || $db->num_rows($resql) == 0) {
|
|||
}
|
||||
|
||||
$section = $db->fetch_object($resql);
|
||||
$doc_id = $section->doc_id;
|
||||
$facture_id = $section->fk_facture;
|
||||
|
||||
$db->begin();
|
||||
|
||||
// 1. Hole alle Sections (sortiert)
|
||||
$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 .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$sql .= " AND line_type = 'section'";
|
||||
$sql .= " ORDER BY line_order";
|
||||
$resql = $db->query($sql);
|
||||
|
|
@ -65,21 +42,18 @@ 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;
|
||||
}
|
||||
|
|
@ -92,49 +66,44 @@ $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();
|
||||
|
||||
// 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)
|
||||
// Freie Produkte zuerst
|
||||
$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 .= " AND line_order < ".(int)$first_section_order;
|
||||
$sql .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$sql .= " AND line_type = 'product'";
|
||||
$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;
|
||||
$processed[$obj->rowid] = true;
|
||||
dol_syslog('[SubtotalTitle] move_section: Freie Zeile VOR Sections #'.$obj->rowid.' → line_order='.$new_order, LOG_INFO);
|
||||
$new_order++;
|
||||
}
|
||||
|
||||
// 2. SECTIONS in neuer Reihenfolge (nach dem Swap)
|
||||
// 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
|
||||
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 ".$tables['fk_parent']." = ".(int)$doc_id;
|
||||
$sql .= " AND document_type = '".$db->escape($docType)."'";
|
||||
$sql .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$sql .= " AND line_type = 'product'";
|
||||
$sql .= " AND parent_section = ".(int)$sec_id;
|
||||
$sql .= " ORDER BY line_order";
|
||||
|
|
@ -142,14 +111,12 @@ foreach ($sections as $sec_id) {
|
|||
|
||||
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 ".$tables['fk_parent']." = ".(int)$doc_id;
|
||||
$sql .= " AND document_type = '".$db->escape($docType)."'";
|
||||
$sql .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$sql .= " AND line_type = 'text'";
|
||||
$sql .= " AND parent_section = ".(int)$sec_id;
|
||||
$sql .= " ORDER BY line_order";
|
||||
|
|
@ -157,39 +124,18 @@ foreach ($sections as $sec_id) {
|
|||
|
||||
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 ".$tables['fk_parent']." = ".(int)$doc_id;
|
||||
$sql .= " AND document_type = '".$db->escape($docType)."'";
|
||||
$sql .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$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++;
|
||||
}
|
||||
}
|
||||
|
|
@ -202,30 +148,21 @@ foreach ($updates as $rowid => $order) {
|
|||
$db->query($sql);
|
||||
}
|
||||
|
||||
// 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";
|
||||
// 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'";
|
||||
$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 = "UPDATE ".MAIN_DB_PREFIX."facturedet";
|
||||
$sql_upd .= " SET rang = ".$rang;
|
||||
$sql_upd .= " WHERE rowid = ".(int)$detail_id;
|
||||
$sql_upd .= " WHERE rowid = ".(int)$obj->fk_facturedet;
|
||||
$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();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,112 +2,24 @@
|
|||
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 . ', docType=' . $docType);
|
||||
subtotaltitle_debug_log('🔓 remove_from_section: product=' . $product_id);
|
||||
|
||||
if (!$product_id || !$docType) {
|
||||
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
|
||||
if (!$product_id) {
|
||||
echo json_encode(['success' => false, 'error' => 'Missing product_id']);
|
||||
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 ".$tables['fk_line']." = ".(int)$product_id;
|
||||
$sql .= " AND document_type = '".$db->escape($docType)."'";
|
||||
$db->query($sql);
|
||||
$sql .= " WHERE fk_facturedet = ".(int)$product_id;
|
||||
|
||||
subtotaltitle_debug_log('✅ Produkt #' . $product_id . ' aus Section #'.$old_section_id.' entfernt');
|
||||
|
||||
// 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();
|
||||
$result = $db->query($sql);
|
||||
|
||||
if ($result) {
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => $db->lasterror()]);
|
||||
}
|
||||
|
|
@ -2,26 +2,17 @@
|
|||
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 || !$docType) {
|
||||
if (!$facture_id || !$new_order_json) {
|
||||
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) {
|
||||
|
|
@ -44,7 +35,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 ".$tables['fk_line']." = ".(int)$item['id'];
|
||||
$sql .= " WHERE fk_facturedet = ".(int)$item['id'];
|
||||
$db->query($sql);
|
||||
subtotaltitle_debug_log(' Produkt #'.$item['id'].' → order='.$item['order'].', section='.($item['parent_section'] ?: 'FREI'));
|
||||
|
||||
|
|
@ -62,70 +53,29 @@ 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 ".$tables['fk_parent']." = ".(int)$facture_id."
|
||||
AND document_type = '".$db->escape($docType)."'
|
||||
WHERE fk_facture = ".(int)$facture_id."
|
||||
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 document_type = '".$db->escape($docType)."'";
|
||||
AND line_type = 'product'";
|
||||
$res_max = $db->query($sql_max);
|
||||
$obj_max = $db->fetch_object($res_max);
|
||||
|
||||
if ($obj_max && $obj_max->max_order) {
|
||||
// Subtotal kommt direkt nach dem letzten Produkt: max_order + 0.5
|
||||
$temp_order = (float)$obj_max->max_order + 0.5;
|
||||
// Subtotal bekommt hohe Nummer (wird gleich normalisiert)
|
||||
$temp_order = (int)$obj_max->max_order * 100 + 50;
|
||||
|
||||
$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.' (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.')');
|
||||
subtotaltitle_debug_log(' Subtotal #'.$subtotal->rowid.' → temp_order='.$temp_order.' (nach Section '.$subtotal->parent_section.')');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,8 +83,7 @@ while ($section = $db->fetch_object($res_sections)) {
|
|||
subtotaltitle_debug_log('🔢 Normalisiere line_order...');
|
||||
|
||||
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager
|
||||
WHERE ".$tables['fk_parent']." = ".(int)$facture_id."
|
||||
AND document_type = '".$db->escape($docType)."'
|
||||
WHERE fk_facture = ".(int)$facture_id."
|
||||
ORDER BY line_order";
|
||||
$resql = $db->query($sql);
|
||||
|
||||
|
|
@ -147,23 +96,19 @@ while ($obj = $db->fetch_object($resql)) {
|
|||
$new_order_num++;
|
||||
}
|
||||
|
||||
// ========== 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
|
||||
// ========== 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'";
|
||||
$sql .= " ORDER BY line_order";
|
||||
$resql = $db->query($sql);
|
||||
|
||||
$rang = 1;
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
$fk_line_value = $obj->{$tables['fk_line']};
|
||||
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
|
||||
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facturedet";
|
||||
$sql_upd .= " SET rang = ".$rang;
|
||||
$sql_upd .= " WHERE rowid = ".(int)$fk_line_value;
|
||||
$sql_upd .= " WHERE rowid = ".(int)$obj->fk_facturedet;
|
||||
$db->query($sql_upd);
|
||||
subtotaltitle_debug_log(' Sync rang: '.$obj->line_type.' #'.$fk_line_value.' → rang='.$rang);
|
||||
$rang++;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
<?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'
|
||||
]);
|
||||
203
ajax/sync_to_facturedet.php
Executable file → Normal file
203
ajax/sync_to_facturedet.php
Executable file → Normal file
|
|
@ -13,7 +13,6 @@ 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');
|
||||
|
||||
|
|
@ -21,22 +20,14 @@ $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.', docType='.$docType);
|
||||
subtotaltitle_debug_log('🔄 sync_to_facturedet: action='.$action.', line_id='.$line_id.', type='.$line_type);
|
||||
|
||||
if (!$line_id || !$action || !$docType) {
|
||||
if (!$line_id || !$action) {
|
||||
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,
|
||||
|
|
@ -60,48 +51,15 @@ if ($action == 'add') {
|
|||
}
|
||||
|
||||
$line = $db->fetch_object($resql);
|
||||
$document_id = $line->{$tables['fk_parent']};
|
||||
$facture_id = $line->fk_facture;
|
||||
$line_type = $line->line_type;
|
||||
$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'));
|
||||
// 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'));
|
||||
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;
|
||||
|
||||
|
|
@ -125,9 +83,8 @@ 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.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
|
||||
$sql_sum .= " INNER JOIN ".MAIN_DB_PREFIX."facturedet d ON d.rowid = m.fk_facturedet";
|
||||
$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);
|
||||
|
|
@ -138,38 +95,31 @@ if ($action == 'add') {
|
|||
break;
|
||||
}
|
||||
|
||||
// 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
|
||||
// Bestimme rang (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 .= " INNER JOIN ".MAIN_DB_PREFIX."facturedet d ON d.rowid = m.fk_facturedet";
|
||||
$sql_rang .= " WHERE m.fk_facture = ".(int)$facture_id;
|
||||
$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.$tables['lines_table'];
|
||||
$sql_shift = "UPDATE ".MAIN_DB_PREFIX."facturedet";
|
||||
$sql_shift .= " SET rang = rang + 1";
|
||||
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
||||
$sql_shift .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$sql_shift .= " AND rang >= ".(int)$new_rang;
|
||||
$db->query($sql_shift);
|
||||
|
||||
// Füge neue Zeile in Detail-Tabelle ein
|
||||
// Füge neue Zeile in facturedet ein
|
||||
subtotaltitle_debug_log('📝 INSERT: line_type='.$line_type.', special_code='.$special_code);
|
||||
|
||||
$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 = "INSERT INTO ".MAIN_DB_PREFIX."facturedet";
|
||||
$sql_ins .= " (fk_facture, 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)$document_id.", ";
|
||||
$sql_ins .= (int)$facture_id.", ";
|
||||
$sql_ins .= "'".$db->escape($description)."', ";
|
||||
$sql_ins .= (float)$qty.", ";
|
||||
$sql_ins .= ($line_type == 'subtotal') ? (float)$total_ht.", " : "0, ";
|
||||
|
|
@ -189,28 +139,28 @@ if ($action == 'add') {
|
|||
exit;
|
||||
}
|
||||
|
||||
$new_detail_id = $db->last_insert_id(MAIN_DB_PREFIX.$tables['lines_table']);
|
||||
$new_facturedet_id = $db->last_insert_id(MAIN_DB_PREFIX."facturedet");
|
||||
|
||||
// Update unsere Manager-Tabelle
|
||||
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql_upd .= " SET ".$tables['fk_line']." = ".(int)$new_detail_id;
|
||||
$sql_upd .= " SET fk_facturedet = ".(int)$new_facturedet_id;
|
||||
$sql_upd .= ", in_facturedet = 1";
|
||||
$sql_upd .= " WHERE rowid = ".(int)$line_id;
|
||||
$db->query($sql_upd);
|
||||
|
||||
subtotaltitle_debug_log('✅ Zeile #'.$line_id.' zu '.$tables['lines_table'].' hinzugefügt als #'.$new_detail_id);
|
||||
subtotaltitle_debug_log('✅ Zeile #'.$line_id.' zu facturedet hinzugefügt als #'.$new_facturedet_id);
|
||||
|
||||
echo json_encode(array(
|
||||
'success' => true,
|
||||
'detail_id' => $new_detail_id,
|
||||
'facturedet_id' => $new_facturedet_id,
|
||||
'rang' => $new_rang
|
||||
));
|
||||
|
||||
} elseif ($action == 'remove') {
|
||||
// ========== AUS DETAIL-TABELLE ENTFERNEN ==========
|
||||
// ========== AUS RECHNUNG ENTFERNEN ==========
|
||||
|
||||
// Hole Daten
|
||||
$sql = "SELECT ".$tables['fk_line']." as detail_id, ".$tables['fk_parent']." as parent_id, line_type FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql = "SELECT fk_facturedet, fk_facture, line_type FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql .= " WHERE rowid = ".(int)$line_id;
|
||||
$resql = $db->query($sql);
|
||||
|
||||
|
|
@ -227,20 +177,20 @@ if ($action == 'add') {
|
|||
exit;
|
||||
}
|
||||
|
||||
if (!$line->detail_id) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Not in detail table'));
|
||||
if (!$line->fk_facturedet) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Not in facturedet'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Hole rang bevor wir löschen
|
||||
$sql_rang = "SELECT rang FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".(int)$line->detail_id;
|
||||
$sql_rang = "SELECT rang FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid = ".(int)$line->fk_facturedet;
|
||||
$res_rang = $db->query($sql_rang);
|
||||
$obj_rang = $db->fetch_object($res_rang);
|
||||
$old_rang = $obj_rang ? $obj_rang->rang : 0;
|
||||
|
||||
// Lösche aus Detail-Tabelle
|
||||
$sql_del = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
|
||||
$sql_del .= " WHERE rowid = ".(int)$line->detail_id;
|
||||
// Lösche aus facturedet
|
||||
$sql_del = "DELETE FROM ".MAIN_DB_PREFIX."facturedet";
|
||||
$sql_del .= " WHERE rowid = ".(int)$line->fk_facturedet;
|
||||
|
||||
if (!$db->query($sql_del)) {
|
||||
echo json_encode(array('success' => false, 'error' => $db->lasterror()));
|
||||
|
|
@ -249,28 +199,28 @@ if ($action == 'add') {
|
|||
|
||||
// Schließe Lücke in rang
|
||||
if ($old_rang > 0) {
|
||||
$sql_shift = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
|
||||
$sql_shift = "UPDATE ".MAIN_DB_PREFIX."facturedet";
|
||||
$sql_shift .= " SET rang = rang - 1";
|
||||
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$line->parent_id;
|
||||
$sql_shift .= " WHERE fk_facture = ".(int)$line->fk_facture;
|
||||
$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 ".$tables['fk_line']." = NULL";
|
||||
$sql_upd .= " SET fk_facturedet = NULL";
|
||||
$sql_upd .= ", in_facturedet = 0";
|
||||
$sql_upd .= " WHERE rowid = ".(int)$line_id;
|
||||
$db->query($sql_upd);
|
||||
|
||||
subtotaltitle_debug_log('✅ Zeile #'.$line_id.' aus '.$tables['lines_table'].' entfernt');
|
||||
subtotaltitle_debug_log('✅ Zeile #'.$line_id.' aus facturedet entfernt');
|
||||
|
||||
echo json_encode(array('success' => true));
|
||||
|
||||
} elseif ($action == 'update_subtotal') {
|
||||
// ========== SUBTOTAL-BETRAG AKTUALISIEREN ==========
|
||||
|
||||
$sql = "SELECT m.".$tables['fk_line']." as detail_id, m.parent_section, s.title as section_title";
|
||||
$sql = "SELECT m.fk_facturedet, 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;
|
||||
|
|
@ -284,108 +234,33 @@ if ($action == 'add') {
|
|||
|
||||
$line = $db->fetch_object($resql);
|
||||
|
||||
if (!$line->detail_id) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Not in detail table'));
|
||||
if (!$line->fk_facturedet) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Not in facturedet'));
|
||||
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.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
|
||||
$sql_sum .= " INNER JOIN ".MAIN_DB_PREFIX."facturedet d ON d.rowid = m.fk_facturedet";
|
||||
$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 Detail-Tabelle
|
||||
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
|
||||
// Update facturedet
|
||||
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facturedet";
|
||||
$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->detail_id;
|
||||
$sql_upd .= " WHERE rowid = ".(int)$line->fk_facturedet;
|
||||
$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'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,32 +2,23 @@
|
|||
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 . ', docType=' . $docType);
|
||||
subtotaltitle_debug_log('🔢 toggle_subtotal: section=' . $section_id . ', show=' . $show);
|
||||
|
||||
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']);
|
||||
if (!$section_id) {
|
||||
echo json_encode(['success' => false, 'error' => 'Missing section_id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Hole Section-Info
|
||||
$sql = "SELECT fk_facture, fk_propal, fk_commande, title, document_type FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql = "SELECT fk_facture, title 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->{$tables['fk_parent']};
|
||||
$facture_id = $section->fk_facture;
|
||||
|
||||
$db->begin();
|
||||
|
||||
|
|
@ -48,9 +39,8 @@ 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.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
|
||||
$sql_sum .= " INNER JOIN ".MAIN_DB_PREFIX."facturedet d ON d.rowid = m.fk_facturedet";
|
||||
$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);
|
||||
|
|
@ -64,63 +54,45 @@ if ($show) {
|
|||
$obj_last = $db->fetch_object($res_last);
|
||||
$last_order = $obj_last->max_order ? $obj_last->max_order : 0;
|
||||
|
||||
// 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)
|
||||
// Neue line_order = nach letztem Produkt
|
||||
$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 ".$tables['fk_parent']." = ".(int)$facture_id;
|
||||
$sql_shift .= " AND document_type = '".$db->escape($docType)."'";
|
||||
$sql_shift .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$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, 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())";
|
||||
$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())";
|
||||
$db->query($sql_ins);
|
||||
$subtotal_manager_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");
|
||||
|
||||
// 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
|
||||
// Subtotal-Zeile auch direkt in facturedet einfügen
|
||||
// Bestimme rang (nach letztem Produkt der Section)
|
||||
$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)$facture_id;
|
||||
$sql_rang .= " AND m.document_type = '".$db->escape($docType)."'";
|
||||
$sql_rang .= " AND m.line_order < ".(int)$new_order;
|
||||
$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'";
|
||||
$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.$tables['lines_table'];
|
||||
$sql_shift_fd = "UPDATE ".MAIN_DB_PREFIX."facturedet";
|
||||
$sql_shift_fd .= " SET rang = rang + 1";
|
||||
$sql_shift_fd .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
|
||||
$sql_shift_fd .= " WHERE fk_facture = ".(int)$facture_id;
|
||||
$sql_shift_fd .= " AND rang >= ".(int)$new_rang;
|
||||
$db->query($sql_shift_fd);
|
||||
|
||||
// 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)";
|
||||
// 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)";
|
||||
$sql_ins_fd .= " VALUES (";
|
||||
$sql_ins_fd .= (int)$facture_id.", ";
|
||||
$sql_ins_fd .= "'".$db->escape('Zwischensumme: '.$section->title)."', ";
|
||||
|
|
@ -133,22 +105,17 @@ 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, "; // 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.")";
|
||||
$sql_ins_fd .= "0)";
|
||||
$db->query($sql_ins_fd);
|
||||
$new_detail_id = $db->last_insert_id(MAIN_DB_PREFIX.$tables['lines_table']);
|
||||
$new_facturedet_id = $db->last_insert_id(MAIN_DB_PREFIX."facturedet");
|
||||
|
||||
// Verknüpfe Manager-Eintrag mit Detail-Tabelle
|
||||
// Verknüpfe Manager-Eintrag mit facturedet
|
||||
$sql_link = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||
$sql_link .= " SET ".$tables['fk_line']." = ".(int)$new_detail_id;
|
||||
$sql_link .= ", in_facturedet = 1";
|
||||
$sql_link .= " SET fk_facturedet = ".(int)$new_facturedet_id;
|
||||
$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 . ' ('.$tables['lines_table'].' #' . $new_detail_id . ', special_code=102)');
|
||||
subtotaltitle_debug_log('✅ Subtotal-Zeile erstellt für Section ' . $section_id . ' mit Summe ' . $subtotal_ht . ' (facturedet #' . $new_facturedet_id . ', special_code=102)');
|
||||
}
|
||||
} else {
|
||||
// Hole erst fk_facturedet bevor wir löschen
|
||||
|
|
|
|||
|
|
@ -1,179 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
894
class/actions_subtotaltitle.class.php
Executable file → Normal file
894
class/actions_subtotaltitle.class.php
Executable file → Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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, Angebote und Kundenaufträge";
|
||||
$this->description = "Positionsgruppen und Zwischensummen für Rechnungen";
|
||||
// Used only if file README.md and README-LL.md not found.
|
||||
$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.";
|
||||
$this->descriptionlong = "Organisieren Sie Rechnungspositionen in Sections mit automatischen Zwischensummen. Fügen Sie Textzeilen hinzu und verwalten Sie komplexe Rechnungen übersichtlich.";
|
||||
|
||||
// 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 = '4.1';
|
||||
$this->version = '1.0';
|
||||
// Url to the file with your last numberversion of this module
|
||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||
|
||||
|
|
@ -122,8 +122,6 @@ 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
Executable file → Normal file
0
core/substitutions/functions_subtotaltitle.lib.php
Executable file → Normal file
|
|
@ -78,11 +78,8 @@ 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
Executable file → Normal file
0
css/subtotaltitle_sync.css
Executable file → Normal file
|
|
@ -1,64 +0,0 @@
|
|||
<?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
BIN
img/grip.png
Binary file not shown.
|
Before Width: | Height: | Size: 90 B |
|
|
@ -3,7 +3,7 @@ $(document).ready(function() {
|
|||
var factureId = getFactureId();
|
||||
if (!factureId) return;
|
||||
|
||||
$.get(subtotaltitleAjaxUrl + 'get_line_orders.php', {
|
||||
$.get('/dolibarr/custom/subtotaltitle/ajax/get_line_orders.php', {
|
||||
facture_id: factureId
|
||||
}, function(response) {
|
||||
if (!response.success) return;
|
||||
|
|
|
|||
1110
js/subtotaltitle.js
1110
js/subtotaltitle.js
File diff suppressed because it is too large
Load diff
178
js/subtotaltitle_sync.js
Executable file → Normal file
178
js/subtotaltitle_sync.js
Executable file → Normal file
|
|
@ -3,20 +3,6 @@
|
|||
// 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)
|
||||
*/
|
||||
|
|
@ -24,27 +10,23 @@ function syncToFacturedet(lineId, lineType) {
|
|||
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
|
||||
debugLog('📤 Sync to facturedet: ' + lineType + ' #' + lineId);
|
||||
|
||||
var docType = getDocumentTypeForSync();
|
||||
debugLog('Document type: ' + docType);
|
||||
|
||||
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
|
||||
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
|
||||
action: 'add',
|
||||
line_id: lineId,
|
||||
line_type: lineType,
|
||||
document_type: docType
|
||||
line_type: lineType
|
||||
}, function(response) {
|
||||
debugLog('Sync response: ' + JSON.stringify(response));
|
||||
if (response.success) {
|
||||
updateSyncCheckbox(lineId, true);
|
||||
debugLog('✅ Zeile zu Rechnung hinzugefügt');
|
||||
} else {
|
||||
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
|
||||
alert((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);
|
||||
showErrorAlert((lang.errorSyncing || 'Fehler beim Synchronisieren') + ': ' + error);
|
||||
alert((lang.errorSyncing || 'Fehler beim Synchronisieren') + ': ' + error);
|
||||
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', false);
|
||||
});
|
||||
}
|
||||
|
|
@ -54,42 +36,32 @@ 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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
var docType = getDocumentTypeForSync();
|
||||
debugLog('Document type: ' + docType);
|
||||
|
||||
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
|
||||
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
|
||||
action: 'remove',
|
||||
line_id: lineId,
|
||||
line_type: lineType,
|
||||
document_type: docType
|
||||
line_type: lineType
|
||||
}, 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'));
|
||||
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);
|
||||
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + error);
|
||||
alert((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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -125,50 +97,46 @@ function updateSyncCheckbox(lineId, isInFacturedet) {
|
|||
*/
|
||||
function syncAllToFacturedet() {
|
||||
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
|
||||
|
||||
var $unchecked = $('.sync-checkbox:not(:checked)');
|
||||
var total = $unchecked.length;
|
||||
|
||||
if (total === 0) {
|
||||
showErrorAlert(lang.allElementsAlreadyInInvoice || 'Alle Elemente sind bereits in der Rechnung.');
|
||||
if (!confirm(lang.confirmSyncAll || 'Alle Positionsgruppen-Elemente (Sections, Textzeilen, Zwischensummen) zur Rechnung hinzufügen?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
showConfirmDialog(
|
||||
'Alle zur Rechnung hinzufügen',
|
||||
lang.confirmSyncAll || 'Alle Positionsgruppen-Elemente (Sections, Textzeilen, Zwischensummen) zur Rechnung hinzufügen?',
|
||||
function() {
|
||||
debugLog('📤 Sync ALL to facturedet...');
|
||||
|
||||
var $unchecked = $('.sync-checkbox:not(:checked)');
|
||||
var total = $unchecked.length;
|
||||
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>');
|
||||
if (total === 0) {
|
||||
alert(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');
|
||||
|
||||
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
|
||||
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
|
||||
action: 'add',
|
||||
line_id: lineId,
|
||||
line_type: lineType,
|
||||
document_type: docType
|
||||
line_type: lineType
|
||||
}, function(response) {
|
||||
done++;
|
||||
if (!response.success) {
|
||||
if (response.success) {
|
||||
updateSyncCheckbox(lineId, true);
|
||||
} else {
|
||||
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.');
|
||||
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 {
|
||||
// Direkt reloaden ohne UI-Update
|
||||
safeReload();
|
||||
var msg = (lang.elementsAddedToInvoice || '%s Elemente zur Rechnung hinzugefügt.').replace('%s', total);
|
||||
alert(msg);
|
||||
}
|
||||
}
|
||||
}, 'json').fail(function() {
|
||||
|
|
@ -176,55 +144,60 @@ function syncAllToFacturedet() {
|
|||
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 : {};
|
||||
|
||||
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 docType = getDocumentTypeForSync();
|
||||
var documentId = getFactureId();
|
||||
|
||||
if (!documentId) {
|
||||
showErrorAlert('Fehler: Keine Dokument-ID gefunden');
|
||||
if (!confirm(lang.confirmRemoveAll || 'ALLE Positionsgruppen-Elemente aus der Rechnung entfernen?\n\nDie Elemente bleiben in der Verwaltung erhalten.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.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 {
|
||||
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
|
||||
debugLog('📥 Remove ALL from facturedet...');
|
||||
|
||||
var $checked = $('.sync-checkbox:checked');
|
||||
var total = $checked.length;
|
||||
var done = 0;
|
||||
var errors = 0;
|
||||
|
||||
if (total === 0) {
|
||||
alert(lang.noElementsInInvoice || 'Keine Elemente in der Rechnung vorhanden.');
|
||||
return;
|
||||
}
|
||||
}, 'json').fail(function(xhr, status, error) {
|
||||
debugLog('AJAX Fehler: ' + status + ' ' + error);
|
||||
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + error);
|
||||
|
||||
$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 (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);
|
||||
} else {
|
||||
var msg = (lang.elementsRemovedFromInvoice || '%s Elemente aus Rechnung entfernt.').replace('%s', total);
|
||||
alert(msg);
|
||||
}
|
||||
}
|
||||
}, 'json').fail(function() {
|
||||
done++;
|
||||
errors++;
|
||||
});
|
||||
});
|
||||
},
|
||||
'Ja, alle entfernen',
|
||||
'Abbrechen'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -242,15 +215,12 @@ function updateAllSubtotals() {
|
|||
return;
|
||||
}
|
||||
|
||||
var docType = getDocumentTypeForSync();
|
||||
|
||||
$subtotals.each(function() {
|
||||
var lineId = $(this).data('line-id');
|
||||
|
||||
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
|
||||
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
|
||||
action: 'update_subtotal',
|
||||
line_id: lineId,
|
||||
document_type: docType
|
||||
line_id: lineId
|
||||
}, function(response) {
|
||||
done++;
|
||||
debugLog('Subtotal #' + lineId + ' updated: ' + JSON.stringify(response));
|
||||
|
|
|
|||
19
langs/de_DE/subtotaltitle.lang
Executable file → Normal file
19
langs/de_DE/subtotaltitle.lang
Executable file → Normal file
|
|
@ -85,10 +85,10 @@ ButtonCancel = Abbrechen
|
|||
#
|
||||
# UI Elements - Section Actions
|
||||
#
|
||||
SectionCreate = Produktgruppe erstellen
|
||||
SectionEdit = Produktgruppe bearbeiten
|
||||
SectionDelete = Produktgruppe löschen
|
||||
SectionName = Name der Produktgruppe
|
||||
SectionCreate = Section erstellen
|
||||
SectionEdit = Section bearbeiten
|
||||
SectionDelete = Section löschen
|
||||
SectionName = Section-Name
|
||||
SectionSubtotal = Zwischensumme anzeigen
|
||||
ProductCount = Produkte
|
||||
|
||||
|
|
@ -143,14 +143,3 @@ 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
|
||||
|
|
|
|||
|
|
@ -85,10 +85,10 @@ ButtonCancel = Cancel
|
|||
#
|
||||
# UI Elements - Section Actions
|
||||
#
|
||||
SectionCreate = Create product group
|
||||
SectionEdit = Edit product group
|
||||
SectionDelete = Delete product group
|
||||
SectionName = Product group name
|
||||
SectionCreate = Create section
|
||||
SectionEdit = Edit section
|
||||
SectionDelete = Delete section
|
||||
SectionName = Section name
|
||||
SectionSubtotal = Show subtotal
|
||||
ProductCount = Products
|
||||
|
||||
|
|
@ -143,14 +143,3 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
-- ============================================================================
|
||||
-- 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;
|
||||
4
subtotaltitle.kdev4
Executable file
4
subtotaltitle.kdev4
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
[Project]
|
||||
CreatedFrom=
|
||||
Manager=KDevCustomBuildSystem
|
||||
Name=subtotaltitle
|
||||
Loading…
Reference in a new issue