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

View file

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

View file

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

View file

@ -6,26 +6,44 @@ if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../mai
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php"; if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
if (!$res) die("Include of main fails"); if (!$res) die("Include of main fails");
require_once __DIR__.'/../lib/subtotaltitle.lib.php'; require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
$textline_id = GETPOST('textline_id', 'int'); $textline_id = GETPOST('textline_id', 'int');
$text = GETPOST('text', 'restricthtml'); $text = GETPOST('text', 'restricthtml');
subtotaltitle_debug_log('🔄 edit_textline: id=' . $textline_id); subtotaltitle_debug_log('edit_textline: id=' . $textline_id);
if (!$textline_id || !$text) { if (!$textline_id || !$text) {
echo json_encode(array('success' => false, 'error' => 'Missing parameters')); echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
exit; exit;
} }
// Hole erst fk_facturedet (falls Textzeile in Rechnung ist) // Hole erst document_type und FK zur Detail-Tabelle
$sql_get = "SELECT fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $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 .= " WHERE rowid = ".(int)$textline_id;
$sql_get .= " AND line_type = 'text'"; $sql_get .= " AND line_type = 'text'";
$resql = $db->query($sql_get); $resql = $db->query($sql_get);
$obj = $db->fetch_object($resql); $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 // Update Manager-Tabelle
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager"; $sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
@ -38,15 +56,15 @@ if (!$db->query($sql)) {
exit; exit;
} }
// Falls in facturedet vorhanden, dort auch updaten // Falls in Detail-Tabelle vorhanden, dort auch updaten
if ($fk_facturedet > 0) { if ($fk_detail > 0 && $tables) {
$sql_fd = "UPDATE ".MAIN_DB_PREFIX."facturedet"; $sql_fd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_fd .= " SET description = '".$db->escape($text)."'"; $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); $db->query($sql_fd);
subtotaltitle_debug_log('✅ Textzeile + facturedet geändert'); subtotaltitle_debug_log('Textzeile + Detail geaendert (docType='.$docType.')');
} else { } 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,53 +2,66 @@
define('NOTOKENRENEWAL', 1); define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php'; require '../../../main.inc.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.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'); $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); $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']); echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit; exit;
} }
// Prüfe Rechnungsstatus // Hole die richtigen Tabellennamen fuer diesen Dokumenttyp
$facture = new Facture($db); $tables = DocumentTypeHelper::getTableNames($docType);
$facture->fetch($facture_id); if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
if ($facture->statut != Facture::STATUS_DRAFT) {
echo json_encode(['success' => false, 'error' => 'Rechnung ist nicht im Entwurf']);
exit; 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(); $db->begin();
$deleted = 0; $deleted = 0;
foreach ($line_ids as $line_id) { foreach ($line_ids as $line_id) {
$line_id = (int)$line_id; $line_id = (int)$line_id;
// Aus facturedet löschen // Aus Detail-Tabelle loeschen
$sql = "DELETE FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid = ".$line_id; $sql = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".$line_id;
if ($db->query($sql)) { if ($db->query($sql)) {
$deleted++; $deleted++;
subtotaltitle_debug_log('✅ Zeile gelöscht: ' . $line_id); subtotaltitle_debug_log('Zeile geloescht: ' . $line_id);
} }
// Aus Manager-Tabelle löschen // Aus Manager-Tabelle loeschen
$sql_manager = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE fk_facturedet = ".$line_id; $sql_manager = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE ".$tables['fk_line']." = ".$line_id;
$db->query($sql_manager); $db->query($sql_manager);
} }
// Summen neu berechnen // Summen neu berechnen
$facture->update_price(1); $object->update_price(1);
// line_order neu durchnummerieren // line_order neu durchnummerieren
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $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"; $sql .= " ORDER BY line_order";
$resql = $db->query($sql); $resql = $db->query($sql);
@ -60,21 +73,24 @@ while ($obj = $db->fetch_object($resql)) {
} }
// rang synchronisieren // rang synchronisieren
$sql = "SELECT fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql = "SELECT ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id; $sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND line_type = 'product'"; $sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND ".$tables['fk_line']." IS NOT NULL";
$sql .= " ORDER BY line_order"; $sql .= " ORDER BY line_order";
$resql = $db->query($sql); $resql = $db->query($sql);
$rang = 1; $rang = 1;
while ($obj = $db->fetch_object($resql)) { 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) {
$db->query($sql_upd); $sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table']." SET rang = ".$rang." WHERE rowid = ".(int)$obj->detail_id;
$rang++; $db->query($sql_upd);
$rang++;
}
} }
$db->commit(); $db->commit();
subtotaltitle_debug_log('🗑️ Massenlöschung abgeschlossen: ' . $deleted . ' von ' . count($line_ids)); subtotaltitle_debug_log('Massenloeschung abgeschlossen: ' . $deleted . ' von ' . count($line_ids));
echo json_encode(['success' => true, 'deleted' => $deleted]); echo json_encode(['success' => true, 'deleted' => $deleted]);

View file

@ -122,4 +122,58 @@ class DocumentTypeHelper
return isset($contexts[$type]) ? $contexts[$type] : ''; 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; global $db;
$tables = DocumentTypeHelper::getTableNames($docType); $tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) return ""; 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 // Lade Übersetzungen
$langs->load('subtotaltitle@subtotaltitle'); $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 // CSS
$cssPath = dol_buildpath('/custom/subtotaltitle/css/subtotaltitle.css', 1); $cssPath = dol_buildpath('/custom/subtotaltitle/css/subtotaltitle.css', 1);
echo '<link rel="stylesheet" type="text/css" href="'.$cssPath.'">'."\n"; echo '<link rel="stylesheet" type="text/css" href="'.$cssPath.'">'."\n";
@ -148,6 +164,9 @@ class ActionsSubtotalTitle extends CommonHookActions
// Übersetzungen als JavaScript-Variablen bereitstellen // Übersetzungen als JavaScript-Variablen bereitstellen
echo '<script type="text/javascript">'."\n"; echo '<script type="text/javascript">'."\n";
echo 'var subtotalTitleIsDraft = '.($this->isDraft ? 'true' : 'false').';'."\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 'var subtotalTitleLang = {'."\n";
echo ' buttonCreateTextline: '.json_encode($langs->trans('ButtonCreateTextline')).','."\n"; echo ' buttonCreateTextline: '.json_encode($langs->trans('ButtonCreateTextline')).','."\n";
echo ' buttonToInvoice: '.json_encode($langs->trans('ButtonToInvoice')).','."\n"; echo ' buttonToInvoice: '.json_encode($langs->trans('ButtonToInvoice')).','."\n";
@ -230,14 +249,14 @@ class ActionsSubtotalTitle extends CommonHookActions
// Sync-Buttons + Collapse-Buttons - rechts ausgerichtet // Sync-Buttons + Collapse-Buttons - rechts ausgerichtet
echo '<script>$(document).ready(function() {'; echo '<script>$(document).ready(function() {';
echo ' if ($(".sync-collapse-row").length === 0) {'; 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 ' 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="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 ' 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)
echo ' buttons += \'<a class="button" href="#" onclick="expandAllSections(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonExpandAll + \'</a>\';'; if ($hasSections) {
echo ' buttons += \'<a class="button" href="#" onclick="collapseAllSections(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonCollapseAll + \'</a>\';'; echo ' buttons += \'<a class="button" href="#" onclick="expandAllSections(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonExpandAll + \'</a>\';';
echo ' }'; echo ' buttons += \'<a class="button" href="#" onclick="collapseAllSections(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonCollapseAll + \'</a>\';';
}
echo ' buttons += \'</div>\';'; echo ' buttons += \'</div>\';';
echo ' $(".tabsAction").first().after(buttons);'; echo ' $(".tabsAction").first().after(buttons);';
echo ' }'; echo ' }';
@ -261,9 +280,68 @@ class ActionsSubtotalTitle extends CommonHookActions
/** /**
* Overload the doActions function * Overload the doActions function
* Reagiert auf Lösch-Aktionen um die Manager-Tabelle zu aktualisieren
*/ */
public function doActions($parameters, &$object, &$action, $hookmanager) 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; return 0;
} }
@ -626,60 +704,55 @@ class ActionsSubtotalTitle extends CommonHookActions
} }
} }
// Hole ALLE Sections ZWISCHEN letztem rang und aktuellem rang // Hole ALLE Sections und Textzeilen die VOR dieser Produktzeile kommen
$sql = "SELECT DISTINCT s.rowid, s.title, s.show_subtotal, s.collapsed, s.line_order, s.in_facturedet,"; // Kombiniert nach line_order sortiert, damit Textzeilen VOR Sections erscheinen können
$sql .= " (SELECT MIN(fd.rang) FROM ".MAIN_DB_PREFIX."facture_lines_manager m2"; $sql_combined = "SELECT rowid, title, line_type, line_order, show_subtotal, collapsed, in_facturedet,";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." fd ON fd.rowid = m2.".$tables['fk_line']; $sql_combined .= " (SELECT MIN(fd.rang) FROM ".MAIN_DB_PREFIX."facture_lines_manager m2";
$sql .= " WHERE m2.parent_section = s.rowid AND m2.document_type = '".$db->escape($docType)."') as first_product_rang"; $sql_combined .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." fd ON fd.rowid = m2.".$tables['fk_line'];
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager s"; $sql_combined .= " WHERE m2.parent_section = ".MAIN_DB_PREFIX."facture_lines_manager.rowid";
$sql .= $this->getDocumentWhere($document_id, $docType, 's'); $sql_combined .= " AND m2.document_type = '".$db->escape($docType)."') as first_product_rang";
$sql .= " AND s.line_type = 'section'"; $sql_combined .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " HAVING first_product_rang > ".(int)$last_rang[$doc_key]; $sql_combined .= $this->getDocumentWhere($document_id, $docType, '');
$sql .= " AND first_product_rang <= ".(int)$current_rang; $sql_combined .= " AND (line_type = 'section' OR line_type = 'text')";
$sql .= " ORDER BY first_product_rang"; $sql_combined .= " AND line_order < ".(int)$current_line_order;
$resql = $db->query($sql); $sql_combined .= " ORDER BY line_order";
$resql_combined = $db->query($sql_combined);
while ($obj = $db->fetch_object($resql)) {
if (!in_array($obj->rowid, self::$rendered_sections[$doc_key])) {
$section = array(
'section_id' => $obj->rowid,
'title' => $obj->title,
'show_subtotal' => $obj->show_subtotal,
'collapsed' => $obj->collapsed,
'in_facturedet' => $obj->in_facturedet
);
echo $this->renderSectionHeader($section);
self::$rendered_sections[$doc_key][] = $obj->rowid;
if ($this->debug) {
error_log('[SubtotalTitle] ✅ Section "'.$obj->title.'" gerendert vor rang '.$current_rang);
}
}
}
// 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 = $db->fetch_object($resql_combined)) {
while ($obj_text = $db->fetch_object($resql_text)) { if ($obj->line_type == 'section') {
$text_key = 'text_'.$obj_text->rowid; // 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,
'title' => $obj->title,
'show_subtotal' => $obj->show_subtotal,
'collapsed' => $obj->collapsed,
'in_facturedet' => $obj->in_facturedet
);
echo $this->renderSectionHeader($section);
self::$rendered_sections[$doc_key][] = $obj->rowid;
if ($this->debug) {
error_log('[SubtotalTitle] ✅ Section "'.$obj->title.'" gerendert vor rang '.$current_rang);
}
}
}
} elseif ($obj->line_type == 'text') {
// Textzeile rendern
$text_key = 'text_'.$obj->rowid;
if (!in_array($text_key, self::$rendered_sections[$doc_key])) { if (!in_array($text_key, self::$rendered_sections[$doc_key])) {
$textline = array( $textline = array(
'id' => $obj_text->rowid, 'id' => $obj->rowid,
'title' => $obj_text->title, 'title' => $obj->title,
'line_order' => $obj_text->line_order, 'line_order' => $obj->line_order,
'in_facturedet' => $obj_text->in_facturedet 'in_facturedet' => $obj->in_facturedet
); );
echo $this->renderTextLine($textline); echo $this->renderTextLine($textline);
self::$rendered_sections[$doc_key][] = $text_key; self::$rendered_sections[$doc_key][] = $text_key;
if ($this->debug) { if ($this->debug) {
error_log('[SubtotalTitle] ✅ Textzeile "'.$obj_text->title.'" gerendert'); error_log('[SubtotalTitle] ✅ Textzeile "'.$obj->title.'" gerendert');
} }
} }
} }
@ -752,20 +825,24 @@ class ActionsSubtotalTitle extends CommonHookActions
$in_class = $in_facturedet ? ' in-facturedet' : ''; $in_class = $in_facturedet ? ' in-facturedet' : '';
$html = '<tr class="textline-row drag'.$in_class.'" data-textline-id="'.$textline['id'].'" data-line-order="'.$textline['line_order'].'">'; $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;">';
$html .= htmlspecialchars($textline['title']);
// Sync-Checkbox (NEU!) - nur im Entwurfsstatus // Titel (colspan=6) - wie bei Sections
$html .= '<td colspan="6" style="padding:8px; font-weight:bold;">';
$html .= htmlspecialchars($textline['title']);
$html .= '</td>';
// Sync-Checkbox (colspan=2) - wie bei Sections
$html .= '<td colspan="2" align="right">';
if ($this->isDraft) { 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 .= '<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 .= ' 📄</label>';
} }
$html .= '</td>'; $html .= '</td>';
// Leer (colspan=2) - Platzhalter wie bei Sections für Move-Buttons
$html .= '<td colspan="2"></td>';
// Edit (Spalte 11) - nur im Entwurfsstatus // Edit (Spalte 11) - nur im Entwurfsstatus
$html .= '<td class="linecoledit center">'; $html .= '<td class="linecoledit center">';
if ($this->isDraft) { if ($this->isDraft) {
@ -784,15 +861,15 @@ class ActionsSubtotalTitle extends CommonHookActions
// Move (Spalte 13) // Move (Spalte 13)
$html .= '<td class="linecolmove tdlineupdown center"></td>'; $html .= '<td class="linecolmove tdlineupdown center"></td>';
// Unlink (Spalte 14) // Unlink (Spalte 14)
$html .= '<td class="linecolunlink"></td>'; $html .= '<td class="linecolunlink"></td>';
$html .= '</tr>'; $html .= '</tr>';
return $html; return $html;
} }
/** /**
* Rendert eine Subtotal-Zeile * Rendert eine Subtotal-Zeile
*/ */

View file

@ -78,8 +78,11 @@ tr.textline-row {
tr.textline-row .linecolmove { tr.textline-row .linecolmove {
cursor: move; cursor: move;
min-width: 20px;
} }
/* Drag-Handle wird über JavaScript als background-image gesetzt (wie Dolibarr) */
tr.textline-row:hover { tr.textline-row:hover {
background-color: #f5f5f5 !important; 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) * Seite neu laden ohne POST-Warnung (GET-Request)
*/ */
@ -101,32 +280,39 @@ function getFactureId() {
*/ */
function createNewSection() { function createNewSection() {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
var title = prompt(lang.sectionName || 'Neue Positionsgruppe - Name eingeben:');
if (!title) return;
var docInfo = getDocumentInfo(); var docInfo = getDocumentInfo();
if (!docInfo.id) { if (!docInfo.id) {
alert(lang.errorLoadingSections || 'Fehler: Keine Dokument-ID gefunden'); showErrorAlert(lang.errorLoadingSections || 'Fehler: Keine Dokument-ID gefunden');
return; return;
} }
debugLog('Erstelle Section: ' + title + ' für ' + docInfo.type + ' ID ' + docInfo.id); 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', { $.post('/dolibarr/custom/subtotaltitle/ajax/create_section.php', {
facture_id: docInfo.id, facture_id: docInfo.id,
document_type: docInfo.type, document_type: docInfo.type,
title: title title: title
}, function(response) { }, function(response) {
debugLog('Section erstellt: ' + JSON.stringify(response)); debugLog('Section erstellt: ' + JSON.stringify(response));
if (response.success) { if (response.success) {
window.location.href = window.location.pathname + window.location.search; window.location.href = window.location.pathname + window.location.search;
} else { } else {
alert((lang.errorSavingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); showErrorAlert((lang.errorSavingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
} }
}, 'json').fail(function(xhr, status, error) { }, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + 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,25 +349,32 @@ function moveSection(sectionId, direction) {
*/ */
function renameSection(sectionId, currentTitle) { function renameSection(sectionId, currentTitle) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
var newTitle = prompt(lang.sectionName || 'Positionsgruppe umbenennen:', currentTitle);
if (!newTitle) return;
debugLog('✏️ Benenne Section ' + sectionId + ' um zu: ' + newTitle); 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', { $.post('/dolibarr/custom/subtotaltitle/ajax/rename_section.php', {
section_id: sectionId, section_id: sectionId,
title: newTitle title: newTitle
}, function(response) { }, function(response) {
debugLog('Rename response: ' + JSON.stringify(response)); debugLog('Rename response: ' + JSON.stringify(response));
if (response.success) { if (response.success) {
window.location.href = window.location.pathname + window.location.search; window.location.href = window.location.pathname + window.location.search;
} else { } else {
alert((lang.errorSavingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); showErrorAlert((lang.errorSavingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
} }
}, 'json').fail(function(xhr, status, error) { }, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + 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) { function deleteSection(sectionId) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmDeleteSection || 'Leere Positionsgruppe löschen?')) {
return;
}
debugLog('🗑️ Lösche leere Section ' + sectionId); 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', { $.post('/dolibarr/custom/subtotaltitle/ajax/delete_section.php', {
section_id: sectionId, section_id: sectionId,
force: 0 // Nur leere löschen force: 0,
}, function(response) { document_type: getDocumentType()
debugLog('Delete response: ' + JSON.stringify(response)); }, function(response) {
if (response.success) { debugLog('Delete response: ' + JSON.stringify(response));
window.location.href = window.location.pathname + window.location.search; if (response.success) {
} else { window.location.href = window.location.pathname + window.location.search;
alert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } else {
} showErrorAlert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}, 'json').fail(function(xhr, status, error) { }
debugLog('AJAX Fehler: ' + status + ' ' + error); }, 'json').fail(function(xhr, status, error) {
alert((lang.errorDeletingSection || 'Fehler beim Löschen') + ': ' + error); debugLog('AJAX Fehler: ' + status + ' ' + 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 productCount = $row.data('product-count');
var productIds = $row.data('product-ids') || []; 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); var msg1 = (lang.confirmDeleteSectionForce || 'Wollen Sie wirklich die Positionsgruppe UND alle %s enthaltenen Produkte löschen?').replace('%s', productCount);
if (!confirm(msg1)) {
return;
}
var msg2 = (lang.confirmDeleteSectionForce2 || 'Sind Sie WIRKLICH sicher?\n\n%s Produkte werden unwiderruflich gelöscht!').replace('%s', productCount); showConfirmDialog(
if (!confirm(msg2)) { 'Achtung - Positionsgruppe löschen',
return; '<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', { $.post('/dolibarr/custom/subtotaltitle/ajax/delete_section.php', {
section_id: sectionId, section_id: sectionId,
force: 1, force: 1,
product_ids: JSON.stringify(productIds) product_ids: JSON.stringify(productIds),
}, function(response) { document_type: getDocumentType()
debugLog('Force-Delete response: ' + JSON.stringify(response)); }, function(response) {
if (response.success) { debugLog('Force-Delete response: ' + JSON.stringify(response));
window.location.href = window.location.pathname + window.location.search; if (response.success) {
} else { window.location.href = window.location.pathname + window.location.search;
alert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } else {
} showErrorAlert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}, 'json').fail(function(xhr, status, error) { }
debugLog('AJAX Fehler: ' + status + ' ' + error); }, 'json').fail(function(xhr, status, error) {
alert((lang.errorDeletingSection || 'Fehler beim Löschen') + ': ' + error); debugLog('AJAX Fehler: ' + status + ' ' + 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 * Fügt leere Sections an die richtige Stelle in der Tabelle ein
@ -971,6 +1181,16 @@ function reinitTableDnD() {
var $table = $('#tablelines'); var $table = $('#tablelines');
if ($table.length && typeof $.fn.tableDnD !== 'undefined') { 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 // Neu initialisieren
$table.tableDnD({ $table.tableDnD({
onDragClass: 'myDragClass', 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'); debugLog('✅ tableDnD neu initialisiert');
} }
} }
@ -1063,32 +1289,39 @@ function insertTextLine(textline) {
*/ */
function createTextLine() { function createTextLine() {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
var text = prompt(lang.textlineContent || 'Text eingeben:');
if (!text) return;
var docInfo = getDocumentInfo(); var docInfo = getDocumentInfo();
if (!docInfo.id) { if (!docInfo.id) {
alert(lang.errorLoadingSections || 'Fehler: Keine Dokument-ID gefunden'); showErrorAlert(lang.errorLoadingSections || 'Fehler: Keine Dokument-ID gefunden');
return; return;
} }
debugLog('Erstelle Textzeile für ' + docInfo.type + ' ID ' + docInfo.id); 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', { $.post('/dolibarr/custom/subtotaltitle/ajax/create_textline.php', {
facture_id: docInfo.id, facture_id: docInfo.id,
document_type: docInfo.type, document_type: docInfo.type,
text: text text: text
}, function(response) { }, function(response) {
debugLog('Textzeile erstellt: ' + JSON.stringify(response)); debugLog('Textzeile erstellt: ' + JSON.stringify(response));
if (response.success) { if (response.success) {
window.location.href = window.location.pathname + window.location.search; window.location.href = window.location.pathname + window.location.search;
} else { } else {
alert((lang.errorSavingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); showErrorAlert((lang.errorSavingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
} }
}, 'json').fail(function(xhr, status, error) { }, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + 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,25 +1329,32 @@ function createTextLine() {
*/ */
function editTextLine(textlineId, currentText) { function editTextLine(textlineId, currentText) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
var newText = prompt(lang.textlineContent || 'Text bearbeiten:', currentText || '');
if (!newText) return;
debugLog('✏️ Bearbeite Textzeile ' + textlineId); showInputDialog(
lang.buttonEdit || 'Textzeile bearbeiten',
lang.textlineContent || 'Text:',
currentText || '',
function(newText) {
debugLog('✏️ Bearbeite Textzeile ' + textlineId);
$.post('/dolibarr/custom/subtotaltitle/ajax/edit_textline.php', { $.post('/dolibarr/custom/subtotaltitle/ajax/edit_textline.php', {
textline_id: textlineId, textline_id: textlineId,
text: newText text: newText
}, function(response) { }, function(response) {
debugLog('Edit response: ' + JSON.stringify(response)); debugLog('Edit response: ' + JSON.stringify(response));
if (response.success) { if (response.success) {
window.location.href = window.location.pathname + window.location.search; window.location.href = window.location.pathname + window.location.search;
} else { } else {
alert((lang.errorSavingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); showErrorAlert((lang.errorSavingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
} }
}, 'json').fail(function(xhr, status, error) { }, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + 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) { function deleteTextLine(textlineId) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; 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', { $.post('/dolibarr/custom/subtotaltitle/ajax/delete_textline.php', {
textline_id: textlineId textline_id: textlineId,
}, function(response) { document_type: getDocumentType()
debugLog('Delete response: ' + JSON.stringify(response)); }, function(response) {
if (response.success) { debugLog('Delete response: ' + JSON.stringify(response));
window.location.href = window.location.pathname + window.location.search; if (response.success) {
} else { window.location.href = window.location.pathname + window.location.search;
alert((lang.errorDeletingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } else {
} showErrorAlert((lang.errorDeletingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}, 'json').fail(function(xhr, status, error) { }
debugLog('AJAX Fehler: ' + status + ' ' + error); }, 'json').fail(function(xhr, status, error) {
alert((lang.errorDeletingTextline || 'Fehler beim Löschen') + ': ' + error); debugLog('AJAX Fehler: ' + status + ' ' + error);
}); showErrorAlert((lang.errorDeletingTextline || 'Fehler beim Löschen') + ': ' + error);
});
},
'Ja, löschen',
'Abbrechen'
);
} }
/** /**
@ -1148,27 +1394,32 @@ function deleteTextLine(textlineId) {
*/ */
function removeFromSection(productId) { function removeFromSection(productId) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmRemoveFromSection || 'Produkt aus Positionsgruppe entfernen?')) {
return;
}
var docType = getDocumentType(); showConfirmDialog(
debugLog('🔓 Entferne Produkt ' + productId + ' aus Section (docType: ' + docType + ')'); 'Aus Positionsgruppe entfernen',
lang.confirmRemoveFromSection || 'Produkt aus Positionsgruppe entfernen?',
function() {
var docType = getDocumentType();
debugLog('Entferne Produkt ' + productId + ' aus Section (docType: ' + docType + ')');
$.post('/dolibarr/custom/subtotaltitle/ajax/remove_from_section.php', { $.post('/dolibarr/custom/subtotaltitle/ajax/remove_from_section.php', {
product_id: productId, product_id: productId,
document_type: docType document_type: docType
}, function(response) { }, function(response) {
debugLog('Remove response: ' + JSON.stringify(response)); debugLog('Remove response: ' + JSON.stringify(response));
if (response.success) { if (response.success) {
safeReload(); safeReload();
} else { } else {
alert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); showErrorAlert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
} }
}, 'json').fail(function(xhr, status, error) { }, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error); debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorReordering || 'Fehler') + ': ' + error); showErrorAlert((lang.errorReordering || 'Fehler') + ': ' + error);
}); });
},
'Ja, entfernen',
'Abbrechen'
);
} }
function toggleMassDelete() { 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( $('#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="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 // Original-Button verstecken
@ -1217,30 +1468,31 @@ function deleteMassSelected() {
}); });
if (selectedIds.length === 0) { if (selectedIds.length === 0) {
alert(lang.noLinesSelected || 'Keine Zeilen ausgewählt!'); showErrorAlert(lang.noLinesSelected || 'Keine Zeilen ausgewählt!');
return; return;
} }
var msg1 = (lang.confirmDeleteLines || 'Wirklich %s Zeilen löschen?').replace('%s', selectedIds.length); var msg1 = (lang.confirmDeleteLines || 'Wirklich %s Zeilen löschen?').replace('%s', selectedIds.length);
if (!confirm(msg1)) {
return;
}
var msg2 = (lang.confirmDeleteLinesWarning || 'LETZTE WARNUNG: %s Zeilen werden UNWIDERRUFLICH gelöscht!').replace('%s', selectedIds.length); showConfirmDialog('Zeilen löschen', msg1, function() {
if (!confirm(msg2)) { // Erste Bestätigung OK - zweite Warnung zeigen
return; var msg2 = (lang.confirmDeleteLinesWarning || 'LETZTE WARNUNG: %s Zeilen werden UNWIDERRUFLICH gelöscht!').replace('%s', selectedIds.length);
}
$.post('/dolibarr/custom/subtotaltitle/ajax/mass_delete.php', { showConfirmDialog('Letzte Warnung', '<div style="color:#c00;font-weight:bold;">' + msg2 + '</div>', function() {
line_ids: JSON.stringify(selectedIds), // Zweite Bestätigung OK - jetzt löschen
facture_id: getFactureId() $.post('/dolibarr/custom/subtotaltitle/ajax/mass_delete.php', {
}, function(response) { line_ids: JSON.stringify(selectedIds),
if (response.success) { facture_id: getFactureId(),
safeReload(); document_type: getDocumentType()
} else { }, function(response) {
alert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannt')); if (response.success) {
} safeReload();
}, 'json'); } else {
showErrorAlert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannt'));
}
}, 'json');
}, 'Endgültig löschen', 'Abbrechen');
}, 'Ja, löschen', 'Abbrechen');
} }
function getFactureId() { function getFactureId() {
@ -1396,35 +1648,38 @@ function linkToNearestSection(lineId) {
} }
if (!targetSection) { if (!targetSection) {
alert('Fehler: Keine passende Section gefunden'); showErrorAlert('Keine passende Positionsgruppe gefunden');
return; return;
} }
// Bestätigung // Bestätigung
if (!confirm('Produkt zur Positionsgruppe "' + targetSection.title + '" hinzufügen?')) { showConfirmDialog(
return; 'Zur Positionsgruppe hinzufügen',
} 'Produkt zur Positionsgruppe "' + targetSection.title + '" hinzufügen?',
function() {
debugLog(' AJAX Call: add_to_section.php');
debugLog(' → AJAX Call: add_to_section.php'); // AJAX Call zum Backend
$.post('/dolibarr/custom/subtotaltitle/ajax/add_to_section.php', {
line_id: lineId,
section_id: targetSection.id,
document_id: docInfo.id,
document_type: docInfo.type
}, function(response) {
debugLog(' Response: ' + JSON.stringify(response));
// AJAX Call zum Backend if (response.success) {
$.post('/dolibarr/custom/subtotaltitle/ajax/add_to_section.php', { window.location.reload();
line_id: lineId, } else {
section_id: targetSection.id, showErrorAlert('Fehler: ' + (response.error || 'Unbekannter Fehler'));
document_id: docInfo.id, }
document_type: docInfo.type }, 'json').fail(function(xhr, status, error) {
}, function(response) { console.error('AJAX Fehler:', status, error);
debugLog(' → Response: ' + JSON.stringify(response)); console.error('Response:', xhr.responseText);
showErrorAlert('Fehler beim Verknüpfen: ' + xhr.responseText);
if (response.success) { });
// Reload Page },
window.location.reload(); 'Ja, hinzufügen',
} else { 'Abbrechen'
alert('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);
});
} }

View file

@ -38,13 +38,13 @@ function syncToFacturedet(lineId, lineType) {
updateSyncCheckbox(lineId, true); updateSyncCheckbox(lineId, true);
debugLog('✅ Zeile zu Rechnung hinzugefügt'); debugLog('✅ Zeile zu Rechnung hinzugefügt');
} else { } else {
alert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
// Checkbox zurücksetzen // Checkbox zurücksetzen
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', false); $('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', false);
} }
}, 'json').fail(function(xhr, status, error) { }, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + 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); $('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', false);
}); });
} }
@ -54,36 +54,42 @@ function syncToFacturedet(lineId, lineType) {
*/ */
function removeFromFacturedet(lineId, lineType) { function removeFromFacturedet(lineId, lineType) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmRemoveLine || 'Zeile aus der Rechnung entfernen?\n\nDie Zeile bleibt in der Positionsgruppen-Verwaltung erhalten.')) {
// Checkbox zurücksetzen
$('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', true);
return;
}
debugLog('📥 Remove from facturedet: ' + lineType + ' #' + lineId); showConfirmDialog(
'Aus Rechnung entfernen',
lang.confirmRemoveLine || 'Zeile aus der Rechnung entfernen?<br><br><em>Die Zeile bleibt in der Positionsgruppen-Verwaltung erhalten.</em>',
function() {
debugLog('📥 Remove from facturedet: ' + lineType + ' #' + lineId);
var docType = getDocumentTypeForSync(); var docType = getDocumentTypeForSync();
debugLog('Document type: ' + docType); debugLog('Document type: ' + docType);
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', { $.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
action: 'remove', action: 'remove',
line_id: lineId, line_id: lineId,
line_type: lineType, line_type: lineType,
document_type: docType document_type: docType
}, function(response) { }, function(response) {
debugLog('Remove response: ' + JSON.stringify(response)); debugLog('Remove response: ' + JSON.stringify(response));
if (response.success) { if (response.success) {
updateSyncCheckbox(lineId, false); updateSyncCheckbox(lineId, false);
debugLog('✅ Zeile aus Rechnung entfernt'); debugLog('✅ Zeile aus Rechnung entfernt');
} else { } 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); $('.sync-checkbox[data-line-id="' + lineId + '"]').prop('checked', true);
} }
}, 'json').fail(function(xhr, status, error) { }, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + 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); $('.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);
} }
/** /**
@ -91,7 +97,7 @@ function removeFromFacturedet(lineId, lineType) {
*/ */
function toggleFacturedetSync(lineId, lineType, checkbox) { function toggleFacturedetSync(lineId, lineType, checkbox) {
if (event) event.stopPropagation(); if (event) event.stopPropagation();
if (checkbox.checked) { if (checkbox.checked) {
syncToFacturedet(lineId, lineType); syncToFacturedet(lineId, lineType);
} else { } else {
@ -105,7 +111,7 @@ function toggleFacturedetSync(lineId, lineType, checkbox) {
function updateSyncCheckbox(lineId, isInFacturedet) { function updateSyncCheckbox(lineId, isInFacturedet) {
var $checkbox = $('.sync-checkbox[data-line-id="' + lineId + '"]'); var $checkbox = $('.sync-checkbox[data-line-id="' + lineId + '"]');
$checkbox.prop('checked', isInFacturedet); $checkbox.prop('checked', isInFacturedet);
var $row = $checkbox.closest('tr'); var $row = $checkbox.closest('tr');
if (isInFacturedet) { if (isInFacturedet) {
$row.addClass('in-facturedet'); $row.addClass('in-facturedet');
@ -119,56 +125,58 @@ function updateSyncCheckbox(lineId, isInFacturedet) {
*/ */
function syncAllToFacturedet() { function syncAllToFacturedet() {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; 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 $unchecked = $('.sync-checkbox:not(:checked)');
var total = $unchecked.length; var total = $unchecked.length;
var done = 0;
var errors = 0;
if (total === 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; return;
} }
var docType = getDocumentTypeForSync(); showConfirmDialog(
'Alle zur Rechnung hinzufügen',
lang.confirmSyncAll || 'Alle Positionsgruppen-Elemente (Sections, Textzeilen, Zwischensummen) zur Rechnung hinzufügen?',
function() {
debugLog('📤 Sync ALL to facturedet...');
$unchecked.each(function() { var done = 0;
var lineId = $(this).data('line-id'); var errors = 0;
var lineType = $(this).data('line-type'); var docType = getDocumentTypeForSync();
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', { $unchecked.each(function() {
action: 'add', var lineId = $(this).data('line-id');
line_id: lineId, var lineType = $(this).data('line-type');
line_type: lineType,
document_type: docType $.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
}, function(response) { action: 'add',
done++; line_id: lineId,
if (response.success) { line_type: lineType,
updateSyncCheckbox(lineId, true); document_type: docType
} else { }, function(response) {
errors++; done++;
} if (response.success) {
if (done >= total) { updateSyncCheckbox(lineId, true);
debugLog('✅ Sync abgeschlossen: ' + (total - errors) + ' erfolgreich, ' + errors + ' Fehler'); } else {
if (errors > 0) { errors++;
var msg = (lang.elementsAddedWithErrors || '%s von %s Elementen hinzugefügt.\n%s Fehler aufgetreten.') }
.replace('%s', total - errors).replace('%s', total).replace('%s', errors); if (done >= total) {
alert(msg); debugLog('✅ Sync abgeschlossen: ' + (total - errors) + ' erfolgreich, ' + errors + ' Fehler');
} else { if (errors > 0) {
var msg = (lang.elementsAddedToInvoice || '%s Elemente zur Rechnung hinzugefügt.').replace('%s', total); showErrorAlert((total - errors) + ' von ' + total + ' Elementen hinzugefügt. ' + errors + ' Fehler aufgetreten.');
alert(msg); } else {
} safeReload();
} }
}, 'json').fail(function() { }
done++; }, 'json').fail(function() {
errors++; done++;
}); errors++;
}); });
});
},
'Ja, hinzufügen',
'Abbrechen'
);
} }
/** /**
@ -176,56 +184,58 @@ function syncAllToFacturedet() {
*/ */
function removeAllFromFacturedet() { function removeAllFromFacturedet() {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; 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 $checked = $('.sync-checkbox:checked');
var total = $checked.length; var total = $checked.length;
var done = 0;
var errors = 0;
if (total === 0) { if (total === 0) {
alert(lang.noElementsInInvoice || 'Keine Elemente in der Rechnung vorhanden.'); showErrorAlert(lang.noElementsInInvoice || 'Keine Elemente in der Rechnung vorhanden.');
return; return;
} }
var docType = getDocumentTypeForSync(); 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...');
$checked.each(function() { var done = 0;
var lineId = $(this).data('line-id'); var errors = 0;
var lineType = $(this).data('line-type'); var docType = getDocumentTypeForSync();
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', { $checked.each(function() {
action: 'remove', var lineId = $(this).data('line-id');
line_id: lineId, var lineType = $(this).data('line-type');
line_type: lineType,
document_type: docType $.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
}, function(response) { action: 'remove',
done++; line_id: lineId,
if (response.success) { line_type: lineType,
updateSyncCheckbox(lineId, false); document_type: docType
} else { }, function(response) {
errors++; done++;
} if (response.success) {
if (done >= total) { updateSyncCheckbox(lineId, false);
debugLog('✅ Remove abgeschlossen: ' + (total - errors) + ' erfolgreich, ' + errors + ' Fehler'); } else {
if (errors > 0) { errors++;
var msg = (lang.elementsRemovedWithErrors || '%s von %s Elementen entfernt.\n%s Fehler aufgetreten.') }
.replace('%s', total - errors).replace('%s', total).replace('%s', errors); if (done >= total) {
alert(msg); debugLog('✅ Remove abgeschlossen: ' + (total - errors) + ' erfolgreich, ' + errors + ' Fehler');
} else { if (errors > 0) {
var msg = (lang.elementsRemovedFromInvoice || '%s Elemente aus Rechnung entfernt.').replace('%s', total); showErrorAlert((total - errors) + ' von ' + total + ' Elementen entfernt. ' + errors + ' Fehler aufgetreten.');
alert(msg); } else {
} safeReload();
} }
}, 'json').fail(function() { }
done++; }, 'json').fail(function() {
errors++; done++;
}); errors++;
}); });
});
},
'Ja, alle entfernen',
'Abbrechen'
);
} }
/** /**
@ -233,16 +243,16 @@ function removeAllFromFacturedet() {
*/ */
function updateAllSubtotals() { function updateAllSubtotals() {
debugLog('🔄 Update all subtotals...'); debugLog('🔄 Update all subtotals...');
var $subtotals = $('.sync-checkbox[data-line-type="subtotal"]:checked'); var $subtotals = $('.sync-checkbox[data-line-type="subtotal"]:checked');
var total = $subtotals.length; var total = $subtotals.length;
var done = 0; var done = 0;
if (total === 0) { if (total === 0) {
debugLog('Keine Subtotals in facturedet'); debugLog('Keine Subtotals in facturedet');
return; return;
} }
var docType = getDocumentTypeForSync(); var docType = getDocumentTypeForSync();
$subtotals.each(function() { $subtotals.each(function() {

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;