Achtung Do Actions wurde Code eingebaut aufgrund des Fehlerhaften löschens der normalen Produktzeilen

This commit is contained in:
Eduard Wisch 2026-01-29 05:58:41 +01:00
parent d8c77df6e4
commit a1468d359e
15 changed files with 984 additions and 712 deletions

7
.claude/settings.json Normal file
View file

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

View file

@ -40,8 +40,8 @@ $fk_propal = ($docType === 'propal') ? (int)$facture_id : 'NULL';
$fk_commande = ($docType === 'order') ? (int)$facture_id : 'NULL';
$sql = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title, line_order, date_creation)";
$sql .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'text', '".$db->escape($text)."', ".$next_order.", NOW())";
$sql .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title, line_order, in_facturedet, date_creation)";
$sql .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'text', '".$db->escape($text)."', ".$next_order.", 1, NOW())";
if ($db->query($sql)) {
$new_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");

View file

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

View file

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

View file

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

View file

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

View file

@ -122,4 +122,58 @@ class DocumentTypeHelper
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;
}
}

View file

@ -54,7 +54,8 @@ class ActionsSubtotalTitle extends CommonHookActions
global $db;
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) return "";
return " WHERE ".$tableAlias.".".$tables['fk_parent']." = ".(int)$document_id." AND ".$tableAlias.".document_type = '".$db->escape($docType)."'";
$prefix = $tableAlias ? $tableAlias."." : "";
return " WHERE ".$prefix.$tables['fk_parent']." = ".(int)$document_id." AND ".$prefix."document_type = '".$db->escape($docType)."'";
}
/**
@ -141,6 +142,21 @@ class ActionsSubtotalTitle extends CommonHookActions
// Lade Übersetzungen
$langs->load('subtotaltitle@subtotaltitle');
// Prüfe ob Sections existieren (für Collapse-Buttons)
global $db;
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
$hasSections = false;
if ($tables && $object->id) {
$sql_sec = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_sec .= " WHERE ".$tables['fk_parent']." = ".(int)$object->id;
$sql_sec .= " AND document_type = '".$db->escape($this->currentDocType)."'";
$sql_sec .= " AND line_type = 'section'";
$res_sec = $db->query($sql_sec);
if ($res_sec && $obj_sec = $db->fetch_object($res_sec)) {
$hasSections = ($obj_sec->cnt > 0);
}
}
// CSS
$cssPath = dol_buildpath('/custom/subtotaltitle/css/subtotaltitle.css', 1);
echo '<link rel="stylesheet" type="text/css" href="'.$cssPath.'">'."\n";
@ -148,6 +164,9 @@ class ActionsSubtotalTitle extends CommonHookActions
// Übersetzungen als JavaScript-Variablen bereitstellen
echo '<script type="text/javascript">'."\n";
echo 'var subtotalTitleIsDraft = '.($this->isDraft ? 'true' : 'false').';'."\n";
echo 'var subtotalTitleHasSections = '.($hasSections ? 'true' : 'false').';'."\n";
// Grip-Bild-Pfad für Drag&Drop (wie Dolibarr es macht)
echo 'var subtotalTitleGripUrl = "'.DOL_URL_ROOT.'/theme/'.$conf->theme.'/img/grip.png";'."\n";
echo 'var subtotalTitleLang = {'."\n";
echo ' buttonCreateTextline: '.json_encode($langs->trans('ButtonCreateTextline')).','."\n";
echo ' buttonToInvoice: '.json_encode($langs->trans('ButtonToInvoice')).','."\n";
@ -230,14 +249,14 @@ class ActionsSubtotalTitle extends CommonHookActions
// Sync-Buttons + Collapse-Buttons - rechts ausgerichtet
echo '<script>$(document).ready(function() {';
echo ' if ($(".sync-collapse-row").length === 0) {';
echo ' var hasCollapse = $("tr.section-header").length > 0;';
echo ' var buttons = \'<div class="sync-collapse-row" style="text-align:right; margin:5px 0;">\';';
echo ' buttons += \'<a class="button" href="#" onclick="syncAllToFacturedet(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonToInvoice + \'</a>\';';
echo ' buttons += \'<a class="button" href="#" onclick="removeAllFromFacturedet(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonFromInvoice + \'</a>\';';
echo ' if (hasCollapse) {';
// Collapse-Buttons nur wenn Sections existieren (aus PHP)
if ($hasSections) {
echo ' buttons += \'<a class="button" href="#" onclick="expandAllSections(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonExpandAll + \'</a>\';';
echo ' buttons += \'<a class="button" href="#" onclick="collapseAllSections(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonCollapseAll + \'</a>\';';
echo ' }';
}
echo ' buttons += \'</div>\';';
echo ' $(".tabsAction").first().after(buttons);';
echo ' }';
@ -261,9 +280,68 @@ class ActionsSubtotalTitle extends CommonHookActions
/**
* Overload the doActions function
* Reagiert auf Lösch-Aktionen um die Manager-Tabelle zu aktualisieren
*/
public function doActions($parameters, &$object, &$action, $hookmanager)
{
global $db, $conf;
// Reagiere auf Zeilen-Löschung
if ($action == 'confirm_deleteline' && !empty($object->id)) {
$lineid = GETPOST('lineid', 'int');
if ($lineid > 0) {
// Bestimme Dokumenttyp
$docType = 'invoice';
if (get_class($object) == 'Propal') {
$docType = 'propal';
} elseif (get_class($object) == 'Commande') {
$docType = 'order';
}
require_once __DIR__.'/DocumentTypeHelper.class.php';
$tables = DocumentTypeHelper::getTableNames($docType);
if ($tables) {
// Lösche aus Manager-Tabelle
$sql = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_line']." = ".(int)$lineid;
$db->query($sql);
// Nummeriere line_order neu durch
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$object->id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$new_order = 1;
while ($obj = $db->fetch_object($resql)) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager SET line_order = ".$new_order." WHERE rowid = ".(int)$obj->rowid;
$db->query($sql_upd);
$new_order++;
}
// Synchronisiere rang in Detail-Tabelle
$sql = "SELECT rowid, ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$object->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++;
}
}
}
}
}
return 0;
}
@ -626,20 +704,24 @@ class ActionsSubtotalTitle extends CommonHookActions
}
}
// Hole ALLE Sections ZWISCHEN letztem rang und aktuellem rang
$sql = "SELECT DISTINCT s.rowid, s.title, s.show_subtotal, s.collapsed, s.line_order, s.in_facturedet,";
$sql .= " (SELECT MIN(fd.rang) FROM ".MAIN_DB_PREFIX."facture_lines_manager m2";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." fd ON fd.rowid = m2.".$tables['fk_line'];
$sql .= " WHERE m2.parent_section = s.rowid AND m2.document_type = '".$db->escape($docType)."') as first_product_rang";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager s";
$sql .= $this->getDocumentWhere($document_id, $docType, 's');
$sql .= " AND s.line_type = 'section'";
$sql .= " HAVING first_product_rang > ".(int)$last_rang[$doc_key];
$sql .= " AND first_product_rang <= ".(int)$current_rang;
$sql .= " ORDER BY first_product_rang";
$resql = $db->query($sql);
// Hole ALLE Sections und Textzeilen die VOR dieser Produktzeile kommen
// Kombiniert nach line_order sortiert, damit Textzeilen VOR Sections erscheinen können
$sql_combined = "SELECT rowid, title, line_type, line_order, show_subtotal, collapsed, in_facturedet,";
$sql_combined .= " (SELECT MIN(fd.rang) FROM ".MAIN_DB_PREFIX."facture_lines_manager m2";
$sql_combined .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." fd ON fd.rowid = m2.".$tables['fk_line'];
$sql_combined .= " WHERE m2.parent_section = ".MAIN_DB_PREFIX."facture_lines_manager.rowid";
$sql_combined .= " AND m2.document_type = '".$db->escape($docType)."') as first_product_rang";
$sql_combined .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_combined .= $this->getDocumentWhere($document_id, $docType, '');
$sql_combined .= " AND (line_type = 'section' OR line_type = 'text')";
$sql_combined .= " AND line_order < ".(int)$current_line_order;
$sql_combined .= " ORDER BY line_order";
$resql_combined = $db->query($sql_combined);
while ($obj = $db->fetch_object($resql)) {
while ($obj = $db->fetch_object($resql_combined)) {
if ($obj->line_type == 'section') {
// Section nur rendern wenn first_product_rang passt
if ($obj->first_product_rang > (int)$last_rang[$doc_key] && $obj->first_product_rang <= (int)$current_rang) {
if (!in_array($obj->rowid, self::$rendered_sections[$doc_key])) {
$section = array(
'section_id' => $obj->rowid,
@ -656,30 +738,21 @@ class ActionsSubtotalTitle extends CommonHookActions
}
}
}
// Rendere Textzeilen die VOR dieser Produktzeile kommen
$sql_text = "SELECT rowid, title, line_order, in_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_text .= $this->getDocumentWhere($document_id, $docType);
$sql_text .= " AND line_type = 'text'";
$sql_text .= " AND line_order < ".(int)$current_line_order;
$sql_text .= " ORDER BY line_order";
$resql_text = $db->query($sql_text);
if ($resql_text) {
while ($obj_text = $db->fetch_object($resql_text)) {
$text_key = 'text_'.$obj_text->rowid;
} elseif ($obj->line_type == 'text') {
// Textzeile rendern
$text_key = 'text_'.$obj->rowid;
if (!in_array($text_key, self::$rendered_sections[$doc_key])) {
$textline = array(
'id' => $obj_text->rowid,
'title' => $obj_text->title,
'line_order' => $obj_text->line_order,
'in_facturedet' => $obj_text->in_facturedet
'id' => $obj->rowid,
'title' => $obj->title,
'line_order' => $obj->line_order,
'in_facturedet' => $obj->in_facturedet
);
echo $this->renderTextLine($textline);
self::$rendered_sections[$doc_key][] = $text_key;
if ($this->debug) {
error_log('[SubtotalTitle] ✅ Textzeile "'.$obj_text->title.'" gerendert');
error_log('[SubtotalTitle] ✅ Textzeile "'.$obj->title.'" gerendert');
}
}
}
@ -753,19 +826,23 @@ class ActionsSubtotalTitle extends CommonHookActions
$html = '<tr class="textline-row drag'.$in_class.'" data-textline-id="'.$textline['id'].'" data-line-order="'.$textline['line_order'].'">';
// Inhalt (colspan=10)
$html .= '<td colspan="10" style="padding:8px; font-weight:bold;">';
// Titel (colspan=6) - wie bei Sections
$html .= '<td colspan="6" style="padding:8px; font-weight:bold;">';
$html .= htmlspecialchars($textline['title']);
$html .= '</td>';
// Sync-Checkbox (NEU!) - nur im Entwurfsstatus
// Sync-Checkbox (colspan=2) - wie bei Sections
$html .= '<td colspan="2" align="right">';
if ($this->isDraft) {
$html .= ' <label style="margin-left:15px;font-weight:normal;font-size:12px;" title="In Rechnung/PDF anzeigen">';
$html .= '<label style="font-weight:normal;font-size:12px;" title="In Rechnung/PDF anzeigen">';
$html .= '<input type="checkbox" class="sync-checkbox" data-line-id="'.$textline['id'].'" data-line-type="text" '.$sync_checked.' onclick="toggleFacturedetSync('.$textline['id'].', \'text\', this);">';
$html .= ' 📄</label>';
}
$html .= '</td>';
// Leer (colspan=2) - Platzhalter wie bei Sections für Move-Buttons
$html .= '<td colspan="2"></td>';
// Edit (Spalte 11) - nur im Entwurfsstatus
$html .= '<td class="linecoledit center">';
if ($this->isDraft) {

View file

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

BIN
img/grip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

View file

@ -7,6 +7,185 @@ function debugLog(message) {
}
}
/**
* Zeigt einen Dolibarr-styled Bestätigungsdialog
* @param {string} title - Dialogtitel
* @param {string} content - Dialoginhalt (HTML erlaubt)
* @param {function} onConfirm - Callback bei Bestätigung
* @param {string} confirmLabel - Text für Bestätigen-Button (optional)
* @param {string} cancelLabel - Text für Abbrechen-Button (optional)
*/
function showConfirmDialog(title, content, onConfirm, confirmLabel, cancelLabel) {
confirmLabel = confirmLabel || 'Ja';
cancelLabel = cancelLabel || 'Abbrechen';
var dialogId = 'subtotal-confirm-dialog-' + Date.now();
// Entferne vorherige Dialoge
$('.subtotal-confirm-dialog').remove();
var $dialog = $('<div/>', {
id: dialogId,
'class': 'subtotal-confirm-dialog',
title: title
}).appendTo('body');
$dialog.html(content);
$dialog.dialog({
autoOpen: true,
modal: true,
width: 'auto',
minWidth: 350,
dialogClass: 'confirm-dialog-box',
buttons: [
{
text: confirmLabel,
'class': 'button-delete',
style: 'background:#c00;color:#fff;',
click: function() {
$(this).dialog('close');
if (typeof onConfirm === 'function') {
onConfirm();
}
}
},
{
text: cancelLabel,
'class': 'button-cancel',
click: function() {
$(this).dialog('close');
}
}
],
close: function() {
$(this).dialog('destroy').remove();
}
});
}
/**
* Zeigt eine Dolibarr-styled Fehlermeldung
* @param {string} message - Fehlermeldung
*/
function showErrorAlert(message) {
var dialogId = 'subtotal-error-dialog-' + Date.now();
$('.subtotal-error-dialog').remove();
var $dialog = $('<div/>', {
id: dialogId,
'class': 'subtotal-error-dialog',
title: 'Fehler'
}).appendTo('body');
$dialog.html('<div class="error" style="padding:10px;">' + message + '</div>');
$dialog.dialog({
autoOpen: true,
modal: true,
width: 'auto',
minWidth: 300,
dialogClass: 'confirm-dialog-box',
buttons: [
{
text: 'OK',
click: function() {
$(this).dialog('close');
}
}
],
close: function() {
$(this).dialog('destroy').remove();
}
});
}
/**
* Zeigt einen Dolibarr-styled Eingabedialog
* @param {string} title - Dialogtitel
* @param {string} label - Label für das Eingabefeld
* @param {string} defaultValue - Vorausgefüllter Wert (optional)
* @param {function} onConfirm - Callback bei Bestätigung, erhält den eingegebenen Wert
* @param {string} confirmLabel - Text für Bestätigen-Button (optional)
* @param {string} cancelLabel - Text für Abbrechen-Button (optional)
*/
function showInputDialog(title, label, defaultValue, onConfirm, confirmLabel, cancelLabel) {
confirmLabel = confirmLabel || 'OK';
cancelLabel = cancelLabel || 'Abbrechen';
defaultValue = defaultValue || '';
var dialogId = 'subtotal-input-dialog-' + Date.now();
var inputId = 'subtotal-input-' + Date.now();
// Entferne vorherige Dialoge
$('.subtotal-input-dialog').remove();
var $dialog = $('<div/>', {
id: dialogId,
'class': 'subtotal-input-dialog',
title: title
}).appendTo('body');
var content = '<div style="padding:10px 0;">';
content += '<label for="' + inputId + '" style="display:block;margin-bottom:8px;">' + label + '</label>';
content += '<input type="text" id="' + inputId + '" class="flat minwidth300" style="width:100%;padding:8px;" value="' + defaultValue.replace(/"/g, '&quot;') + '">';
content += '</div>';
$dialog.html(content);
$dialog.dialog({
autoOpen: true,
modal: true,
width: 400,
dialogClass: 'confirm-dialog-box',
open: function() {
// Fokus auf Eingabefeld setzen
var $input = $('#' + inputId);
$input.focus().select();
// Enter-Taste zum Bestätigen
$input.on('keypress', function(e) {
if (e.which === 13) {
var value = $(this).val().trim();
if (value) {
$dialog.dialog('close');
if (typeof onConfirm === 'function') {
onConfirm(value);
}
}
}
});
},
buttons: [
{
text: confirmLabel,
'class': 'button-save',
style: 'background:#0077b3;color:#fff;',
click: function() {
var value = $('#' + inputId).val().trim();
if (value) {
$(this).dialog('close');
if (typeof onConfirm === 'function') {
onConfirm(value);
}
}
}
},
{
text: cancelLabel,
'class': 'button-cancel',
click: function() {
$(this).dialog('close');
}
}
],
close: function() {
$(this).dialog('destroy').remove();
}
});
}
/**
* Seite neu laden ohne POST-Warnung (GET-Request)
*/
@ -101,15 +280,18 @@ function getFactureId() {
*/
function createNewSection() {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
var title = prompt(lang.sectionName || 'Neue Positionsgruppe - Name eingeben:');
if (!title) return;
var docInfo = getDocumentInfo();
if (!docInfo.id) {
alert(lang.errorLoadingSections || 'Fehler: Keine Dokument-ID gefunden');
showErrorAlert(lang.errorLoadingSections || 'Fehler: Keine Dokument-ID gefunden');
return;
}
showInputDialog(
lang.sectionCreate || 'Positionsgruppe erstellen',
lang.sectionName || 'Name der Positionsgruppe:',
'',
function(title) {
debugLog('Erstelle Section: ' + title + ' für ' + docInfo.type + ' ID ' + docInfo.id);
$.post('/dolibarr/custom/subtotaltitle/ajax/create_section.php', {
@ -121,12 +303,16 @@ function createNewSection() {
if (response.success) {
window.location.href = window.location.pathname + window.location.search;
} else {
alert((lang.errorSavingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
showErrorAlert((lang.errorSavingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorSavingSection || 'Fehler beim Erstellen') + ': ' + error);
showErrorAlert((lang.errorSavingSection || 'Fehler beim Erstellen') + ': ' + error);
});
},
lang.buttonSave || 'Erstellen',
lang.buttonCancel || 'Abbrechen'
);
}
/**
@ -163,9 +349,12 @@ function moveSection(sectionId, direction) {
*/
function renameSection(sectionId, currentTitle) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
var newTitle = prompt(lang.sectionName || 'Positionsgruppe umbenennen:', currentTitle);
if (!newTitle) return;
showInputDialog(
lang.buttonEdit || 'Positionsgruppe umbenennen',
lang.sectionName || 'Name der Positionsgruppe:',
currentTitle || '',
function(newTitle) {
debugLog('✏️ Benenne Section ' + sectionId + ' um zu: ' + newTitle);
$.post('/dolibarr/custom/subtotaltitle/ajax/rename_section.php', {
@ -176,12 +365,16 @@ function renameSection(sectionId, currentTitle) {
if (response.success) {
window.location.href = window.location.pathname + window.location.search;
} else {
alert((lang.errorSavingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
showErrorAlert((lang.errorSavingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorSavingSection || 'Fehler beim Umbenennen') + ': ' + error);
showErrorAlert((lang.errorSavingSection || 'Fehler beim Umbenennen') + ': ' + error);
});
},
lang.buttonSave || 'Speichern',
lang.buttonCancel || 'Abbrechen'
);
}
/**
@ -189,26 +382,32 @@ function renameSection(sectionId, currentTitle) {
*/
function deleteSection(sectionId) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmDeleteSection || 'Leere Positionsgruppe löschen?')) {
return;
}
showConfirmDialog(
'Positionsgruppe löschen',
lang.confirmDeleteSection || 'Leere Positionsgruppe löschen?',
function() {
debugLog('🗑️ Lösche leere Section ' + sectionId);
$.post('/dolibarr/custom/subtotaltitle/ajax/delete_section.php', {
section_id: sectionId,
force: 0 // Nur leere löschen
force: 0,
document_type: getDocumentType()
}, function(response) {
debugLog('Delete response: ' + JSON.stringify(response));
if (response.success) {
window.location.href = window.location.pathname + window.location.search;
} else {
alert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
showErrorAlert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorDeletingSection || 'Fehler beim Löschen') + ': ' + error);
showErrorAlert((lang.errorDeletingSection || 'Fehler beim Löschen') + ': ' + error);
});
},
'Ja, löschen',
'Abbrechen'
);
}
/**
@ -220,33 +419,44 @@ function deleteSectionForce(sectionId) {
var productCount = $row.data('product-count');
var productIds = $row.data('product-ids') || [];
var msg1 = (lang.confirmDeleteSectionForce || '⚠️ ACHTUNG!\n\nWollen Sie wirklich die Positionsgruppe\nUND alle %s enthaltenen Produkte löschen?\n\nDiese Aktion kann nicht rückgängig gemacht werden!').replace('%s', productCount);
if (!confirm(msg1)) {
return;
}
var msg1 = (lang.confirmDeleteSectionForce || 'Wollen Sie wirklich die Positionsgruppe UND alle %s enthaltenen Produkte löschen?').replace('%s', productCount);
var msg2 = (lang.confirmDeleteSectionForce2 || 'Sind Sie WIRKLICH sicher?\n\n%s Produkte werden unwiderruflich gelöscht!').replace('%s', productCount);
if (!confirm(msg2)) {
return;
}
showConfirmDialog(
'Achtung - Positionsgruppe löschen',
'<div style="color:#c00;"><strong>WARNUNG!</strong></div><br>' + msg1 + '<br><br><em>Diese Aktion kann nicht rückgängig gemacht werden!</em>',
function() {
var msg2 = (lang.confirmDeleteSectionForce2 || 'Sind Sie WIRKLICH sicher? %s Produkte werden unwiderruflich gelöscht!').replace('%s', productCount);
debugLog('🔴 Force-Delete Section ' + sectionId + ' mit Produkten: ' + JSON.stringify(productIds));
showConfirmDialog(
'Letzte Warnung',
'<div style="color:#c00;font-weight:bold;">' + msg2 + '</div>',
function() {
debugLog('Force-Delete Section ' + sectionId + ' mit Produkten: ' + JSON.stringify(productIds));
$.post('/dolibarr/custom/subtotaltitle/ajax/delete_section.php', {
section_id: sectionId,
force: 1,
product_ids: JSON.stringify(productIds)
product_ids: JSON.stringify(productIds),
document_type: getDocumentType()
}, function(response) {
debugLog('Force-Delete response: ' + JSON.stringify(response));
if (response.success) {
window.location.href = window.location.pathname + window.location.search;
} else {
alert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
showErrorAlert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorDeletingSection || 'Fehler beim Löschen') + ': ' + error);
showErrorAlert((lang.errorDeletingSection || 'Fehler beim Löschen') + ': ' + error);
});
},
'Endgültig löschen',
'Abbrechen'
);
},
'Ja, löschen',
'Abbrechen'
);
}
/**
* Fügt leere Sections an die richtige Stelle in der Tabelle ein
@ -971,6 +1181,16 @@ function reinitTableDnD() {
var $table = $('#tablelines');
if ($table.length && typeof $.fn.tableDnD !== 'undefined') {
// Grip-Hintergrundbild für alle tdlineupdown Elemente setzen (wie Dolibarr es macht)
// Das ist nötig für dynamisch hinzugefügte Zeilen
// Verwende die von PHP bereitgestellte URL (identisch zu Dolibarr's ajaxrow.tpl.php)
if (typeof subtotalTitleGripUrl !== 'undefined') {
$(".tdlineupdown").css("background-image", 'url(' + subtotalTitleGripUrl + ')');
$(".tdlineupdown").css("background-repeat", "no-repeat");
$(".tdlineupdown").css("background-position", "center center");
debugLog('🖼️ Grip-Bild gesetzt: ' + subtotalTitleGripUrl);
}
// Neu initialisieren
$table.tableDnD({
onDragClass: 'myDragClass',
@ -990,6 +1210,12 @@ function reinitTableDnD() {
}
});
// Hover-Effekt für Drag-Handle (wie Dolibarr es macht)
$(".tdlineupdown").off("mouseenter mouseleave").hover(
function() { $(this).addClass('showDragHandle'); },
function() { $(this).removeClass('showDragHandle'); }
);
debugLog('✅ tableDnD neu initialisiert');
}
}
@ -1063,15 +1289,18 @@ function insertTextLine(textline) {
*/
function createTextLine() {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
var text = prompt(lang.textlineContent || 'Text eingeben:');
if (!text) return;
var docInfo = getDocumentInfo();
if (!docInfo.id) {
alert(lang.errorLoadingSections || 'Fehler: Keine Dokument-ID gefunden');
showErrorAlert(lang.errorLoadingSections || 'Fehler: Keine Dokument-ID gefunden');
return;
}
showInputDialog(
lang.buttonCreateTextline || 'Textzeile erstellen',
lang.textlineContent || 'Text eingeben:',
'',
function(text) {
debugLog('Erstelle Textzeile für ' + docInfo.type + ' ID ' + docInfo.id);
$.post('/dolibarr/custom/subtotaltitle/ajax/create_textline.php', {
@ -1083,12 +1312,16 @@ function createTextLine() {
if (response.success) {
window.location.href = window.location.pathname + window.location.search;
} else {
alert((lang.errorSavingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
showErrorAlert((lang.errorSavingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorSavingTextline || 'Fehler beim Erstellen') + ': ' + error);
showErrorAlert((lang.errorSavingTextline || 'Fehler beim Erstellen') + ': ' + error);
});
},
lang.buttonSave || 'Erstellen',
lang.buttonCancel || 'Abbrechen'
);
}
/**
@ -1096,9 +1329,12 @@ function createTextLine() {
*/
function editTextLine(textlineId, currentText) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
var newText = prompt(lang.textlineContent || 'Text bearbeiten:', currentText || '');
if (!newText) return;
showInputDialog(
lang.buttonEdit || 'Textzeile bearbeiten',
lang.textlineContent || 'Text:',
currentText || '',
function(newText) {
debugLog('✏️ Bearbeite Textzeile ' + textlineId);
$.post('/dolibarr/custom/subtotaltitle/ajax/edit_textline.php', {
@ -1109,12 +1345,16 @@ function editTextLine(textlineId, currentText) {
if (response.success) {
window.location.href = window.location.pathname + window.location.search;
} else {
alert((lang.errorSavingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
showErrorAlert((lang.errorSavingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorSavingTextline || 'Fehler beim Bearbeiten') + ': ' + error);
showErrorAlert((lang.errorSavingTextline || 'Fehler beim Bearbeiten') + ': ' + error);
});
},
lang.buttonSave || 'Speichern',
lang.buttonCancel || 'Abbrechen'
);
}
/**
@ -1122,25 +1362,31 @@ function editTextLine(textlineId, currentText) {
*/
function deleteTextLine(textlineId) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmDeleteTextline || 'Textzeile wirklich löschen?')) {
return;
}
debugLog('🗑️ Lösche Textzeile ' + textlineId);
showConfirmDialog(
'Textzeile löschen',
lang.confirmDeleteTextline || 'Textzeile wirklich löschen?',
function() {
debugLog('Lösche Textzeile ' + textlineId);
$.post('/dolibarr/custom/subtotaltitle/ajax/delete_textline.php', {
textline_id: textlineId
textline_id: textlineId,
document_type: getDocumentType()
}, function(response) {
debugLog('Delete response: ' + JSON.stringify(response));
if (response.success) {
window.location.href = window.location.pathname + window.location.search;
} else {
alert((lang.errorDeletingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
showErrorAlert((lang.errorDeletingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorDeletingTextline || 'Fehler beim Löschen') + ': ' + error);
showErrorAlert((lang.errorDeletingTextline || 'Fehler beim Löschen') + ': ' + error);
});
},
'Ja, löschen',
'Abbrechen'
);
}
/**
@ -1148,12 +1394,13 @@ function deleteTextLine(textlineId) {
*/
function removeFromSection(productId) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmRemoveFromSection || 'Produkt aus Positionsgruppe entfernen?')) {
return;
}
showConfirmDialog(
'Aus Positionsgruppe entfernen',
lang.confirmRemoveFromSection || 'Produkt aus Positionsgruppe entfernen?',
function() {
var docType = getDocumentType();
debugLog('🔓 Entferne Produkt ' + productId + ' aus Section (docType: ' + docType + ')');
debugLog('Entferne Produkt ' + productId + ' aus Section (docType: ' + docType + ')');
$.post('/dolibarr/custom/subtotaltitle/ajax/remove_from_section.php', {
product_id: productId,
@ -1163,12 +1410,16 @@ function removeFromSection(productId) {
if (response.success) {
safeReload();
} else {
alert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
showErrorAlert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorReordering || 'Fehler') + ': ' + error);
showErrorAlert((lang.errorReordering || 'Fehler') + ': ' + error);
});
},
'Ja, entfernen',
'Abbrechen'
);
}
function toggleMassDelete() {
@ -1186,11 +1437,11 @@ function toggleMassDelete() {
}
});
// Buttons NACH dem Massenlösch-Button einfügen
// Buttons NACH dem Massenlösch-Button einfügen (Ausgewählte löschen ganz rechts)
$('#btnMassDelete').after(
'<a id="btnMassDoDelete" class="butActionDelete" href="#" onclick="deleteMassSelected(); return false;" style="background:#c00;color:#fff;margin-left:5px;">Ausgewählte löschen</a>' +
'<a id="btnMassCancel" class="butAction" href="#" onclick="toggleMassDelete(); return false;">Abbrechen</a>' +
'<a id="btnMassSelectAll" class="butAction" href="#" onclick="selectAllLines(); return false;">Alle auswählen</a>' +
'<a id="btnMassCancel" class="butAction" href="#" onclick="toggleMassDelete(); return false;">Abbrechen</a>'
'<a id="btnMassDoDelete" class="butActionDelete" href="#" onclick="deleteMassSelected(); return false;" style="background:#c00;color:#fff;">Ausgewählte löschen</a>'
);
// Original-Button verstecken
@ -1217,30 +1468,31 @@ function deleteMassSelected() {
});
if (selectedIds.length === 0) {
alert(lang.noLinesSelected || 'Keine Zeilen ausgewählt!');
showErrorAlert(lang.noLinesSelected || 'Keine Zeilen ausgewählt!');
return;
}
var msg1 = (lang.confirmDeleteLines || 'Wirklich %s Zeilen löschen?').replace('%s', selectedIds.length);
if (!confirm(msg1)) {
return;
}
showConfirmDialog('Zeilen löschen', msg1, function() {
// Erste Bestätigung OK - zweite Warnung zeigen
var msg2 = (lang.confirmDeleteLinesWarning || 'LETZTE WARNUNG: %s Zeilen werden UNWIDERRUFLICH gelöscht!').replace('%s', selectedIds.length);
if (!confirm(msg2)) {
return;
}
showConfirmDialog('Letzte Warnung', '<div style="color:#c00;font-weight:bold;">' + msg2 + '</div>', function() {
// Zweite Bestätigung OK - jetzt löschen
$.post('/dolibarr/custom/subtotaltitle/ajax/mass_delete.php', {
line_ids: JSON.stringify(selectedIds),
facture_id: getFactureId()
facture_id: getFactureId(),
document_type: getDocumentType()
}, function(response) {
if (response.success) {
safeReload();
} else {
alert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannt'));
showErrorAlert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannt'));
}
}, 'json');
}, 'Endgültig löschen', 'Abbrechen');
}, 'Ja, löschen', 'Abbrechen');
}
function getFactureId() {
@ -1396,16 +1648,16 @@ function linkToNearestSection(lineId) {
}
if (!targetSection) {
alert('Fehler: Keine passende Section gefunden');
showErrorAlert('Keine passende Positionsgruppe gefunden');
return;
}
// Bestätigung
if (!confirm('Produkt zur Positionsgruppe "' + targetSection.title + '" hinzufügen?')) {
return;
}
debugLog(' AJAX Call: add_to_section.php');
showConfirmDialog(
'Zur Positionsgruppe hinzufügen',
'Produkt zur Positionsgruppe "' + targetSection.title + '" hinzufügen?',
function() {
debugLog(' AJAX Call: add_to_section.php');
// AJAX Call zum Backend
$.post('/dolibarr/custom/subtotaltitle/ajax/add_to_section.php', {
@ -1414,17 +1666,20 @@ function linkToNearestSection(lineId) {
document_id: docInfo.id,
document_type: docInfo.type
}, function(response) {
debugLog(' Response: ' + JSON.stringify(response));
debugLog(' Response: ' + JSON.stringify(response));
if (response.success) {
// Reload Page
window.location.reload();
} else {
alert('Fehler: ' + (response.error || 'Unbekannter Fehler'));
showErrorAlert('Fehler: ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
console.error('AJAX Fehler:', status, error);
console.error('Response:', xhr.responseText);
alert('Fehler beim Verknüpfen: ' + xhr.responseText);
showErrorAlert('Fehler beim Verknüpfen: ' + xhr.responseText);
});
},
'Ja, hinzufügen',
'Abbrechen'
);
}

View file

@ -38,13 +38,13 @@ function syncToFacturedet(lineId, lineType) {
updateSyncCheckbox(lineId, true);
debugLog('✅ Zeile zu Rechnung hinzugefügt');
} else {
alert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
// Checkbox zurücksetzen
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', false);
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorSyncing || 'Fehler beim Synchronisieren') + ': ' + error);
showErrorAlert((lang.errorSyncing || 'Fehler beim Synchronisieren') + ': ' + error);
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', false);
});
}
@ -54,12 +54,11 @@ 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();
@ -76,14 +75,21 @@ function removeFromFacturedet(lineId, lineType) {
updateSyncCheckbox(lineId, false);
debugLog('✅ Zeile aus Rechnung entfernt');
} else {
alert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', true);
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorSyncing || 'Fehler') + ': ' + error);
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + error);
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', true);
});
},
'Ja, entfernen',
'Abbrechen'
);
// Checkbox zurücksetzen bis Bestätigung
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', true);
}
/**
@ -119,22 +125,23 @@ function updateSyncCheckbox(lineId, isInFacturedet) {
*/
function syncAllToFacturedet() {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmSyncAll || 'Alle Positionsgruppen-Elemente (Sections, Textzeilen, Zwischensummen) zur Rechnung hinzufügen?')) {
return;
}
debugLog('📤 Sync ALL to facturedet...');
var $unchecked = $('.sync-checkbox:not(:checked)');
var total = $unchecked.length;
var done = 0;
var errors = 0;
if (total === 0) {
alert(lang.allElementsAlreadyInInvoice || 'Alle Elemente sind bereits in der Rechnung.');
showErrorAlert(lang.allElementsAlreadyInInvoice || 'Alle Elemente sind bereits in der Rechnung.');
return;
}
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 done = 0;
var errors = 0;
var docType = getDocumentTypeForSync();
$unchecked.each(function() {
@ -156,12 +163,9 @@ function syncAllToFacturedet() {
if (done >= total) {
debugLog('✅ Sync abgeschlossen: ' + (total - errors) + ' erfolgreich, ' + errors + ' Fehler');
if (errors > 0) {
var msg = (lang.elementsAddedWithErrors || '%s von %s Elementen hinzugefügt.\n%s Fehler aufgetreten.')
.replace('%s', total - errors).replace('%s', total).replace('%s', errors);
alert(msg);
showErrorAlert((total - errors) + ' von ' + total + ' Elementen hinzugefügt. ' + errors + ' Fehler aufgetreten.');
} else {
var msg = (lang.elementsAddedToInvoice || '%s Elemente zur Rechnung hinzugefügt.').replace('%s', total);
alert(msg);
safeReload();
}
}
}, 'json').fail(function() {
@ -169,6 +173,10 @@ function syncAllToFacturedet() {
errors++;
});
});
},
'Ja, hinzufügen',
'Abbrechen'
);
}
/**
@ -176,22 +184,23 @@ function syncAllToFacturedet() {
*/
function removeAllFromFacturedet() {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmRemoveAll || 'ALLE Positionsgruppen-Elemente aus der Rechnung entfernen?\n\nDie Elemente bleiben in der Verwaltung erhalten.')) {
return;
}
debugLog('📥 Remove ALL from facturedet...');
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.');
showErrorAlert(lang.noElementsInInvoice || 'Keine Elemente in der Rechnung vorhanden.');
return;
}
showConfirmDialog(
'Alle aus Rechnung entfernen',
(lang.confirmRemoveAll || 'ALLE Positionsgruppen-Elemente aus der Rechnung entfernen?') + '<br><br><em>Die Elemente bleiben in der Verwaltung erhalten.</em>',
function() {
debugLog('📥 Remove ALL from facturedet...');
var done = 0;
var errors = 0;
var docType = getDocumentTypeForSync();
$checked.each(function() {
@ -213,12 +222,9 @@ function removeAllFromFacturedet() {
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);
showErrorAlert((total - errors) + ' von ' + total + ' Elementen entfernt. ' + errors + ' Fehler aufgetreten.');
} else {
var msg = (lang.elementsRemovedFromInvoice || '%s Elemente aus Rechnung entfernt.').replace('%s', total);
alert(msg);
safeReload();
}
}
}, 'json').fail(function() {
@ -226,6 +232,10 @@ function removeAllFromFacturedet() {
errors++;
});
});
},
'Ja, alle entfernen',
'Abbrechen'
);
}
/**

View file

@ -1,30 +0,0 @@
-- Fix Foreign Key Constraints für Multi-Document-Support
-- Diese Felder sollten NULL sein können, da eine Section entweder zu einer
-- Rechnung ODER einem Angebot ODER einem Auftrag gehört, aber nie zu allen gleichzeitig.
-- 1. Foreign Key Constraints temporär entfernen
ALTER TABLE llx_facture_lines_manager DROP FOREIGN KEY IF EXISTS `1`;
ALTER TABLE llx_facture_lines_manager DROP FOREIGN KEY IF EXISTS `2`;
ALTER TABLE llx_facture_lines_manager DROP FOREIGN KEY IF EXISTS `3`;
ALTER TABLE llx_facture_lines_manager DROP FOREIGN KEY IF EXISTS `fk_facture_lines_manager_facture`;
ALTER TABLE llx_facture_lines_manager DROP FOREIGN KEY IF EXISTS `fk_facture_lines_manager_propal`;
ALTER TABLE llx_facture_lines_manager DROP FOREIGN KEY IF EXISTS `fk_facture_lines_manager_commande`;
-- 2. Felder auf NULL ändern
ALTER TABLE llx_facture_lines_manager
MODIFY COLUMN fk_facture INT NULL DEFAULT NULL,
MODIFY COLUMN fk_propal INT NULL DEFAULT NULL,
MODIFY COLUMN fk_commande INT NULL DEFAULT NULL;
-- 3. Foreign Key Constraints wieder hinzufügen (mit ON DELETE CASCADE)
ALTER TABLE llx_facture_lines_manager
ADD CONSTRAINT fk_facture_lines_manager_facture
FOREIGN KEY (fk_facture) REFERENCES llx_facture(rowid) ON DELETE CASCADE;
ALTER TABLE llx_facture_lines_manager
ADD CONSTRAINT fk_facture_lines_manager_propal
FOREIGN KEY (fk_propal) REFERENCES llx_propal(rowid) ON DELETE CASCADE;
ALTER TABLE llx_facture_lines_manager
ADD CONSTRAINT fk_facture_lines_manager_commande
FOREIGN KEY (fk_commande) REFERENCES llx_commande(rowid) ON DELETE CASCADE;

View file

@ -1,60 +0,0 @@
-- Copyright (C) 2026 Eduard Wisch
--
-- 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/>.
--
-- Tabelle für Verwaltung von Rechnungs-, Angebots- und Auftragszeilen
--
CREATE TABLE IF NOT EXISTS llx_facture_lines_manager (
rowid INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
-- Dokumenttyp und Referenzen
document_type VARCHAR(20) DEFAULT 'invoice' NOT NULL,
fk_facture INT(11) DEFAULT NULL,
fk_propal INT(11) DEFAULT NULL,
fk_commande INT(11) DEFAULT NULL,
-- Zeilentyp: 'section', 'text', 'subtotal', 'product'
line_type VARCHAR(20) NOT NULL,
-- Referenzen auf Detailzeilen
fk_facturedet INT(11) DEFAULT NULL,
fk_propaldet INT(11) DEFAULT NULL,
fk_commandedet INT(11) DEFAULT NULL,
-- Section-Informationen
parent_section INT(11) DEFAULT NULL,
title VARCHAR(255) DEFAULT NULL,
line_order INT(11) DEFAULT 0,
show_subtotal TINYINT(1) DEFAULT 0,
collapsed TINYINT(1) DEFAULT 0,
in_facturedet TINYINT(1) DEFAULT 0,
-- Timestamps
date_creation DATETIME NOT NULL,
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Indizes
INDEX idx_fk_facture (fk_facture),
INDEX idx_fk_propal (fk_propal),
INDEX idx_fk_commande (fk_commande),
INDEX idx_fk_facturedet (fk_facturedet),
INDEX idx_fk_propaldet (fk_propaldet),
INDEX idx_fk_commandedet (fk_commandedet),
INDEX idx_document_type (document_type),
INDEX idx_line_type (line_type),
INDEX idx_parent_section (parent_section),
INDEX idx_line_order (line_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View file

@ -1,136 +0,0 @@
-- Copyright (C) 2026 Eduard Wisch
--
-- Haupttabelle für Verwaltung von Rechnungs-, Angebots- und Auftragszeilen
-- Diese Datei wird beim Modul-Upgrade ausgeführt
--
-- Prüfen ob Spalten existieren und hinzufügen falls nicht
SET @exist := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'llx_facture_lines_manager'
AND column_name = 'document_type');
SET @sqlstmt := IF(@exist = 0,
'ALTER TABLE llx_facture_lines_manager ADD COLUMN document_type VARCHAR(20) DEFAULT ''invoice'' NOT NULL AFTER fk_facture',
'SELECT ''Column document_type already exists'' as msg');
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- fk_propal hinzufügen
SET @exist := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'llx_facture_lines_manager'
AND column_name = 'fk_propal');
SET @sqlstmt := IF(@exist = 0,
'ALTER TABLE llx_facture_lines_manager ADD COLUMN fk_propal INT(11) DEFAULT NULL AFTER document_type',
'SELECT ''Column fk_propal already exists'' as msg');
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- fk_commande hinzufügen
SET @exist := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'llx_facture_lines_manager'
AND column_name = 'fk_commande');
SET @sqlstmt := IF(@exist = 0,
'ALTER TABLE llx_facture_lines_manager ADD COLUMN fk_commande INT(11) DEFAULT NULL AFTER fk_propal',
'SELECT ''Column fk_commande already exists'' as msg');
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- fk_propaldet hinzufügen
SET @exist := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'llx_facture_lines_manager'
AND column_name = 'fk_propaldet');
SET @sqlstmt := IF(@exist = 0,
'ALTER TABLE llx_facture_lines_manager ADD COLUMN fk_propaldet INT(11) DEFAULT NULL AFTER fk_facturedet',
'SELECT ''Column fk_propaldet already exists'' as msg');
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- fk_commandedet hinzufügen
SET @exist := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'llx_facture_lines_manager'
AND column_name = 'fk_commandedet');
SET @sqlstmt := IF(@exist = 0,
'ALTER TABLE llx_facture_lines_manager ADD COLUMN fk_commandedet INT(11) DEFAULT NULL AFTER fk_propaldet',
'SELECT ''Column fk_commandedet already exists'' as msg');
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Bestehende Daten aktualisieren
UPDATE llx_facture_lines_manager
SET document_type = 'invoice'
WHERE fk_facture IS NOT NULL AND (document_type IS NULL OR document_type = '');
-- Indizes hinzufügen falls sie nicht existieren
SET @exist := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = DATABASE()
AND table_name = 'llx_facture_lines_manager'
AND index_name = 'idx_fk_propal');
SET @sqlstmt := IF(@exist = 0,
'ALTER TABLE llx_facture_lines_manager ADD INDEX idx_fk_propal (fk_propal)',
'SELECT ''Index idx_fk_propal already exists'' as msg');
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @exist := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = DATABASE()
AND table_name = 'llx_facture_lines_manager'
AND index_name = 'idx_fk_commande');
SET @sqlstmt := IF(@exist = 0,
'ALTER TABLE llx_facture_lines_manager ADD INDEX idx_fk_commande (fk_commande)',
'SELECT ''Index idx_fk_commande already exists'' as msg');
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @exist := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = DATABASE()
AND table_name = 'llx_facture_lines_manager'
AND index_name = 'idx_fk_propaldet');
SET @sqlstmt := IF(@exist = 0,
'ALTER TABLE llx_facture_lines_manager ADD INDEX idx_fk_propaldet (fk_propaldet)',
'SELECT ''Index idx_fk_propaldet already exists'' as msg');
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @exist := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = DATABASE()
AND table_name = 'llx_facture_lines_manager'
AND index_name = 'idx_fk_commandedet');
SET @sqlstmt := IF(@exist = 0,
'ALTER TABLE llx_facture_lines_manager ADD INDEX idx_fk_commandedet (fk_commandedet)',
'SELECT ''Index idx_fk_commandedet already exists'' as msg');
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @exist := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = DATABASE()
AND table_name = 'llx_facture_lines_manager'
AND index_name = 'idx_document_type');
SET @sqlstmt := IF(@exist = 0,
'ALTER TABLE llx_facture_lines_manager ADD INDEX idx_document_type (document_type)',
'SELECT ''Index idx_document_type already exists'' as msg');
PREPARE stmt FROM @sqlstmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;