Fehlerbehebungen Drag and Drop, Checkboxen, Anzeige Zwischensumme. Fehler in New Order fix_sections

This commit is contained in:
Eduard Wisch 2026-01-27 13:22:39 +01:00
parent 050f2316b2
commit 954329d701
12 changed files with 718 additions and 183 deletions

View file

@ -46,17 +46,40 @@ $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_line']." = ".(int)$product_id;
$resql = $db->query($sql);
// Hole die line_order der Section (Produkt soll direkt danach kommen)
$sql_section = "SELECT line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE rowid = ".(int)$section_id;
$resql_section = $db->query($sql_section);
$section_order = 1;
if ($obj_section = $db->fetch_object($resql_section)) {
$section_order = $obj_section->line_order;
}
// Berechne neue line_order: Höchste line_order der Produkte in dieser Section + 1
// Oder Section line_order + 1 wenn keine Produkte vorhanden
$sql_max_in_section = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_max_in_section .= " WHERE parent_section = ".(int)$section_id;
$sql_max_in_section .= " AND line_type = 'product'";
$resql_max_section = $db->query($sql_max_in_section);
$obj_max_section = $db->fetch_object($resql_max_section);
if ($obj_max_section && $obj_max_section->max_order) {
$new_line_order = $obj_max_section->max_order + 1;
} else {
$new_line_order = $section_order + 1;
}
subtotaltitle_debug_log(' → Section line_order='.$section_order.', neue Produkt line_order='.$new_line_order);
// Verschiebe alle nachfolgenden Zeilen um 1 nach hinten
$sql_shift = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_shift .= " SET line_order = line_order + 1";
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_shift .= " AND document_type = '".$db->escape($docType)."'";
$sql_shift .= " AND line_order >= ".$new_line_order;
$db->query($sql_shift);
if ($db->num_rows($resql) == 0) {
// Produkt fehlt - hinzufügen
$next_order = 1;
$sql_max = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_max .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_max .= " AND document_type = '".$db->escape($docType)."'";
$resql_max = $db->query($sql_max);
if ($obj = $db->fetch_object($resql_max)) {
$next_order = ($obj->max_order ? $obj->max_order + 1 : 1);
}
// Setze alle FK-Felder explizit (NULL für nicht genutzte)
$fk_facture = ($docType === 'invoice') ? (int)$facture_id : 'NULL';
$fk_propal = ($docType === 'propal') ? (int)$facture_id : 'NULL';
@ -64,20 +87,21 @@ if ($db->num_rows($resql) == 0) {
$sql_ins = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_ins .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, ".$tables['fk_line'].", parent_section, line_order, date_creation)";
$sql_ins .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'product', ".(int)$product_id.", ".(int)$section_id.", ".$next_order.", NOW())";
$sql_ins .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'product', ".(int)$product_id.", ".(int)$section_id.", ".$new_line_order.", NOW())";
$db->query($sql_ins);
subtotaltitle_debug_log(' → Produkt zu Manager-Tabelle hinzugefügt (line_order=' . $next_order . ')');
subtotaltitle_debug_log(' → Produkt zu Manager-Tabelle hinzugefügt (line_order=' . $new_line_order . ')');
} else {
// Produkt existiert - UPDATE parent_section
subtotaltitle_debug_log('🔵🔵🔵 assign_last_product: Produkt #'.$product_id.' → parent_section='.$section_id);
// Produkt existiert - UPDATE parent_section UND line_order
subtotaltitle_debug_log('🔵🔵🔵 assign_last_product: Produkt #'.$product_id.' → parent_section='.$section_id.', line_order='.$new_line_order);
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_upd .= " SET parent_section = ".(int)$section_id;
$sql_upd .= ", line_order = ".$new_line_order;
$sql_upd .= " WHERE ".$tables['fk_line']." = ".(int)$product_id;
$db->query($sql_upd);
subtotaltitle_debug_log(' → parent_section updated');
subtotaltitle_debug_log(' → parent_section und line_order updated');
}
// Neu sortieren

48
ajax/check_subtotal.php Normal file
View file

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

144
ajax/cleanup_subtotals.php Normal file
View file

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

View file

@ -40,12 +40,8 @@ $sql .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->esc
if ($db->query($sql)) {
$section_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");
// Erstelle automatisch auch eine Zwischensumme für diese Section
$subtotal_order = $next_order + 1000; // Hohe Nummer, wird später normalisiert
$sql_subtotal = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_subtotal .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title, parent_section, line_order, date_creation)";
$sql_subtotal .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'subtotal', 'Zwischensumme', ".(int)$section_id.", ".$subtotal_order.", NOW())";
$db->query($sql_subtotal);
// KEIN automatisches Subtotal mehr - wird nur erstellt wenn Checkbox aktiviert wird
// Das Subtotal wird über toggle_subtotal.php erstellt/gelöscht
echo json_encode(['success' => true, 'section_id' => $section_id]);
} else {

58
ajax/fix_sections.php Normal file
View file

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

View file

@ -22,7 +22,7 @@ if (!$tables) {
// Hole ALLE Sections für diesen Dokumenttyp
$sql = "SELECT s.rowid, s.title, s.line_order, ";
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."facture_lines_manager p WHERE p.parent_section = s.rowid) as product_count";
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."facture_lines_manager p WHERE p.parent_section = s.rowid AND p.line_type = 'product' AND p.document_type = '".$db->escape($docType)."') as product_count";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager s";
$sql .= " WHERE s.".$tables['fk_parent']." = ".(int)$facture_id;
$sql .= " AND s.document_type = '".$db->escape($docType)."'";

View file

@ -1,19 +1,41 @@
<?php
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$section_id = GETPOST('section_id', 'int');
$direction = GETPOST('direction', 'alpha');
$docType = GETPOST('document_type', 'alpha');
if (!$section_id || !$direction) {
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
exit;
}
// Wenn kein docType übergeben, versuche ihn aus der Section zu ermitteln
if (!$docType) {
$sql = "SELECT document_type FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE rowid = ".(int)$section_id;
$res = $db->query($sql);
if ($res && $obj = $db->fetch_object($res)) {
$docType = $obj->document_type;
}
}
if (!$docType) {
$docType = 'invoice'; // Fallback
}
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(array('success' => false, 'error' => 'Invalid document type'));
exit;
}
// Hole Section-Info
$sql = "SELECT fk_facture FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql = "SELECT ".$tables['fk_parent']." as doc_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE rowid = ".(int)$section_id;
$sql .= " AND line_type = 'section'";
$sql .= " AND document_type = '".$db->escape($docType)."'";
$resql = $db->query($sql);
if (!$resql || $db->num_rows($resql) == 0) {
@ -22,13 +44,14 @@ if (!$resql || $db->num_rows($resql) == 0) {
}
$section = $db->fetch_object($resql);
$facture_id = $section->fk_facture;
$doc_id = $section->doc_id;
$db->begin();
// 1. Hole alle Sections (sortiert)
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'section'";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
@ -42,18 +65,21 @@ while ($obj = $db->fetch_object($resql)) {
$current_index = array_search($section_id, $sections);
if ($current_index === false) {
$db->rollback();
echo json_encode(array('success' => false, 'error' => 'Section not in list'));
exit;
}
if ($direction == 'up') {
if ($current_index == 0) {
$db->rollback();
echo json_encode(array('success' => false, 'error' => 'Already at top'));
exit;
}
$swap_index = $current_index - 1;
} else {
if ($current_index == count($sections) - 1) {
$db->rollback();
echo json_encode(array('success' => false, 'error' => 'Already at bottom'));
exit;
}
@ -71,7 +97,8 @@ $updates = array();
// Freie Produkte zuerst
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'product'";
$sql .= " AND parent_section IS NULL";
$sql .= " ORDER BY line_order";
@ -84,7 +111,8 @@ while ($obj = $db->fetch_object($resql)) {
// Freie Textzeilen
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'text'";
$sql .= " AND parent_section IS NULL";
$sql .= " ORDER BY line_order";
@ -103,7 +131,8 @@ foreach ($sections as $sec_id) {
// Produkte dieser Section
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'product'";
$sql .= " AND parent_section = ".(int)$sec_id;
$sql .= " ORDER BY line_order";
@ -116,7 +145,8 @@ foreach ($sections as $sec_id) {
// Textzeilen dieser Section
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'text'";
$sql .= " AND parent_section = ".(int)$sec_id;
$sql .= " ORDER BY line_order";
@ -127,9 +157,10 @@ foreach ($sections as $sec_id) {
$new_order++;
}
// ========== SUBTOTAL DIESER SECTION ==========
// Subtotal dieser Section
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'subtotal'";
$sql .= " AND parent_section = ".(int)$sec_id;
$resql = $db->query($sql);
@ -140,6 +171,16 @@ foreach ($sections as $sec_id) {
}
}
// Freie Produkte am Ende (nach allen Sections)
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'product'";
$sql .= " AND parent_section IS NULL";
$sql .= " AND rowid NOT IN (SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE ".$tables['fk_parent']." = ".(int)$doc_id." AND document_type = '".$db->escape($docType)."' AND line_type = 'product' AND parent_section IS NULL AND line_order < (SELECT MIN(line_order) FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE ".$tables['fk_parent']." = ".(int)$doc_id." AND document_type = '".$db->escape($docType)."' AND line_type = 'section'))";
$sql .= " ORDER BY line_order";
// Diese Abfrage ist zu komplex - die freien Produkte wurden bereits oben behandelt
// 4. Führe alle Updates aus
foreach ($updates as $rowid => $order) {
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
@ -148,20 +189,24 @@ foreach ($updates as $rowid => $order) {
$db->query($sql);
}
// 5. Sync rang
$sql = "SELECT fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id;
$sql .= " AND line_type = 'product'";
// 5. Sync rang in Detail-Tabelle
$sql = "SELECT ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND ".$tables['fk_line']." IS NOT NULL";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$rang = 1;
while ($obj = $db->fetch_object($resql)) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facturedet";
$detail_id = $obj->detail_id;
if ($detail_id) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_upd .= " SET rang = ".$rang;
$sql_upd .= " WHERE rowid = ".(int)$obj->fk_facturedet;
$sql_upd .= " WHERE rowid = ".(int)$detail_id;
$db->query($sql_upd);
$rang++;
}
}
$db->commit();

View file

@ -2,23 +2,34 @@
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$product_id = GETPOST('product_id', 'int');
$docType = GETPOST('document_type', 'alpha');
subtotaltitle_debug_log('🔓 remove_from_section: product=' . $product_id);
subtotaltitle_debug_log('🔓 remove_from_section: product=' . $product_id . ', docType=' . $docType);
if (!$product_id) {
echo json_encode(['success' => false, 'error' => 'Missing product_id']);
if (!$product_id || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
// Hole die richtigen Tabellennamen für diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit;
}
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " SET parent_section = NULL";
$sql .= " WHERE fk_facturedet = ".(int)$product_id;
$sql .= " WHERE ".$tables['fk_line']." = ".(int)$product_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$result = $db->query($sql);
if ($result) {
subtotaltitle_debug_log('✅ Produkt #' . $product_id . ' aus Section entfernt');
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => $db->lasterror()]);

View file

@ -62,30 +62,39 @@ foreach ($new_order as $item) {
// ========== SUBTOTALS NEU POSITIONIEREN ==========
subtotaltitle_debug_log('🔢 Repositioniere Subtotals...');
// Hole alle Subtotals für dieses Dokument
$sql = "SELECT rowid, parent_section FROM ".MAIN_DB_PREFIX."facture_lines_manager
WHERE ".$tables['fk_parent']." = ".(int)$facture_id."
AND document_type = '".$db->escape($docType)."'
AND line_type = 'subtotal'";
$resql = $db->query($sql);
$subtotals_to_update = array();
while ($subtotal = $db->fetch_object($resql)) {
$subtotals_to_update[] = $subtotal;
}
foreach ($subtotals_to_update as $subtotal) {
// Finde höchste line_order der Produkte dieser Section
$sql_max = "SELECT MAX(line_order) as max_order
FROM ".MAIN_DB_PREFIX."facture_lines_manager
WHERE parent_section = ".(int)$subtotal->parent_section."
AND line_type = 'product'";
AND line_type = 'product'
AND document_type = '".$db->escape($docType)."'";
$res_max = $db->query($sql_max);
$obj_max = $db->fetch_object($res_max);
if ($obj_max && $obj_max->max_order) {
// Subtotal bekommt hohe Nummer (wird gleich normalisiert)
$temp_order = (int)$obj_max->max_order * 100 + 50;
// Subtotal kommt direkt nach dem letzten Produkt: max_order + 0.5
$temp_order = (float)$obj_max->max_order + 0.5;
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager
SET line_order = ".$temp_order."
WHERE rowid = ".(int)$subtotal->rowid;
$db->query($sql_upd);
subtotaltitle_debug_log(' Subtotal #'.$subtotal->rowid.' → temp_order='.$temp_order.' (nach Section '.$subtotal->parent_section.')');
subtotaltitle_debug_log(' Subtotal #'.$subtotal->rowid.' (Section '.$subtotal->parent_section.') → temp_order='.$temp_order);
} else {
subtotaltitle_debug_log(' ⚠️ Subtotal #'.$subtotal->rowid.' hat keine Produkte in Section '.$subtotal->parent_section);
}
}

View file

@ -518,10 +518,8 @@ class ActionsSubtotalTitle extends CommonHookActions
// Prüfe ob diese Zeile eine unserer speziellen Zeilen ist (Section, Text, Subtotal)
// special_code: 100=Section, 101=Text, 102=Subtotal
if (isset($line->special_code) && in_array($line->special_code, array(100, 101, 102))) {
// Diese Zeile ist eine unserer speziellen Zeilen - per JS ausblenden
echo '<script>$(document).ready(function() { ';
echo ' $("tr[id*=\''.$line->id.'\']").hide();';
echo '});</script>';
// Diese Zeile wird von uns selbst gerendert - Original sofort per CSS verstecken
echo '<style>#row-'.$line->id.', tr[data-line-id="'.$line->id.'"] { display: none !important; }</style>';
return 0;
}
@ -598,8 +596,15 @@ class ActionsSubtotalTitle extends CommonHookActions
$current_parent_section = $obj_current->parent_section;
}
// Subtotal der VORHERIGEN Section rendern (wenn Section-Wechsel)
// Subtotal der VORHERIGEN Section rendern (wenn Section-Wechsel UND show_subtotal aktiviert)
if ($last_parent_section[$doc_key] && $last_parent_section[$doc_key] != $current_parent_section) {
// Prüfe erst ob die Section show_subtotal aktiviert hat
$sql_check_show = "SELECT show_subtotal FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_check_show .= " WHERE rowid = ".(int)$last_parent_section[$doc_key];
$resql_check = $db->query($sql_check_show);
$section_obj = $db->fetch_object($resql_check);
if ($section_obj && $section_obj->show_subtotal) {
$sql_subtotal = "SELECT rowid, title, parent_section, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_subtotal .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_subtotal .= " AND document_type = '".$db->escape($docType)."'";
@ -619,6 +624,7 @@ 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,";
@ -682,6 +688,55 @@ class ActionsSubtotalTitle extends CommonHookActions
// Merke für nächsten Durchlauf
$last_rang[$doc_key] = $current_rang;
$last_parent_section[$doc_key] = $current_parent_section;
// Prüfe ob dies die LETZTE Produktzeile ist - dann Subtotal per JavaScript NACH dieser Zeile einfügen
if ($current_parent_section) {
// Hole max rang für dieses Dokument (nur echte Produkte, keine special_code 100-102)
$sql_max = "SELECT MAX(rang) as max_rang FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_max .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_max .= " AND (special_code IS NULL OR special_code = 0 OR special_code < 100 OR special_code > 102)";
$res_max = $db->query($sql_max);
$obj_max = $db->fetch_object($res_max);
if ($obj_max && $current_rang >= $obj_max->max_rang) {
// Dies ist die letzte Produktzeile - Subtotal per JS NACH dieser Zeile einfügen
$sql_check_show = "SELECT show_subtotal FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_check_show .= " WHERE rowid = ".(int)$current_parent_section;
$resql_check = $db->query($sql_check_show);
$section_obj = $db->fetch_object($resql_check);
if ($section_obj && $section_obj->show_subtotal) {
$sql_subtotal = "SELECT rowid, title, parent_section, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_subtotal .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_subtotal .= " AND document_type = '".$db->escape($docType)."'";
$sql_subtotal .= " AND parent_section = ".(int)$current_parent_section;
$sql_subtotal .= " AND line_type = 'subtotal'";
$resql_subtotal = $db->query($sql_subtotal);
if ($obj_sub = $db->fetch_object($resql_subtotal)) {
$subtotal_key = 'subtotal_'.$obj_sub->rowid;
if (!in_array($subtotal_key, self::$rendered_sections[$doc_key])) {
// Subtotal-HTML generieren
$subtotalHtml = $this->renderSubtotalLine($obj_sub);
// Per JavaScript NACH der aktuellen Zeile einfügen
$escapedHtml = addslashes(str_replace(array("\r", "\n"), '', $subtotalHtml));
echo '<script>$(document).ready(function() { ';
echo ' var $lastRow = $("tr[data-line-id=\''.$line->id.'\']");';
echo ' if ($lastRow.length === 0) $lastRow = $("#row-'.$line->id.'");';
echo ' if ($lastRow.length > 0 && $lastRow.next(".subtotal-row").length === 0) {';
echo ' $lastRow.after("'.$escapedHtml.'");';
echo ' }';
echo '});</script>';
self::$rendered_sections[$doc_key][] = $subtotal_key;
if ($this->debug) {
error_log('[SubtotalTitle] ✅ Subtotal für letzte Section per JS eingefügt');
}
}
}
}
}
}
}
/**
@ -839,18 +894,27 @@ class ActionsSubtotalTitle extends CommonHookActions
$sql_cleanup .= " AND d.rowid IS NULL";
$result = $db->query($sql_cleanup);
// 2. Hole alle Produktzeilen des Dokuments
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
// 2. Hole alle Produktzeilen des Dokuments mit rang
$sql = "SELECT rowid, rang FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " ORDER BY rang";
$resql = $db->query($sql);
$new_products = array();
while ($obj = $db->fetch_object($resql)) {
$sql_check = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_check .= " WHERE ".$tables['fk_line']." = ".(int)$obj->rowid;
$resql_check = $db->query($sql_check);
if ($db->num_rows($resql_check) == 0) {
// Neues Produkt gefunden - merken mit rang
$new_products[] = array('rowid' => $obj->rowid, 'rang' => $obj->rang);
}
}
// 3. Füge neue Produkte ein - am Ende der Liste
if (count($new_products) > 0) {
// Hole einmal die höchste line_order
$next_order = $this->getNextLineOrder($document_id, $docType);
// Setze alle FK-Felder explizit (NULL für nicht genutzte)
@ -858,10 +922,12 @@ class ActionsSubtotalTitle extends CommonHookActions
$fk_propal = ($docType === 'propal') ? (int)$document_id : 'NULL';
$fk_commande = ($docType === 'order') ? (int)$document_id : 'NULL';
foreach ($new_products as $product) {
$sql_ins = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_ins .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, ".$tables['fk_line'].", line_order, date_creation)";
$sql_ins .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'product', ".(int)$obj->rowid.", ".$next_order.", NOW())";
$sql_ins .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'product', ".(int)$product['rowid'].", ".$next_order.", NOW())";
$db->query($sql_ins);
$next_order++; // Für jedes weitere Produkt erhöhen
}
}
}
@ -1202,9 +1268,12 @@ class ActionsSubtotalTitle extends CommonHookActions
global $db;
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) return 1;
$sql = "SELECT MAX(line_order) as max_order";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= $this->getDocumentWhere($document_id, $docType);
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$resql = $db->query($sql);
if (!$resql || !($obj = $db->fetch_object($resql))) {
return 1;

View file

@ -1,5 +1,5 @@
// DEBUG FLAG - true für Debug-Ausgaben, false für Produktiv
var SUBTOTAL_DEBUG = true;
var SUBTOTAL_DEBUG = false;
function debugLog(message) {
if (SUBTOTAL_DEBUG) {
@ -7,6 +7,36 @@ function debugLog(message) {
}
}
/**
* Seite neu laden ohne POST-Warnung (GET-Request)
*/
function safeReload() {
var baseUrl = window.location.href.split('?')[0];
var id = getFactureId();
window.location.href = baseUrl + '?id=' + id;
}
/**
* Bereinigt verwaiste Subtotals und fehlerhafte Einträge (NUR aktuelles Dokument!)
*/
function cleanupOrphanedSubtotals() {
var docInfo = getDocumentInfo();
if (!docInfo.id) return;
$.post('/dolibarr/custom/subtotaltitle/ajax/cleanup_subtotals.php', {
facture_id: docInfo.id,
document_type: docInfo.type
}, function(response) {
if (response.success) {
var total = (response.deleted || 0) + (response.fixed || 0);
if (total > 0) {
debugLog('🧹 Cleanup (Dokument ' + docInfo.id + '): ' + (response.deleted || 0) + ' verwaiste Subtotals, ' + (response.fixed || 0) + ' fehlerhafte Einträge');
// Kein automatischer Reload - nur im Hintergrund bereinigen
}
}
}, 'json');
}
// Flag: Wird gerade gezogen?
var isDragging = false;
var isTogglingSubtotal = false;
@ -20,7 +50,8 @@ if (typeof SubtotalTitleLoaded === 'undefined') {
// Füge Button zu den Standard-Buttons hinzu - NUR im Entwurfsstatus
if ($('#tablelines').length > 0) {
var factureId = getFactureId();
// Cleanup verwaister Subtotals (wo show_subtotal=0)
cleanupOrphanedSubtotals();
// Prüfe ob Dokument im Entwurfsstatus ist
if (typeof subtotalTitleIsDraft !== 'undefined' && subtotalTitleIsDraft === true) {
@ -34,7 +65,6 @@ if (typeof SubtotalTitleLoaded === 'undefined') {
debugLog('⚠️ Dokument nicht im Entwurfsstatus - Button wird nicht angezeigt');
}
// ⬇️ HIER FEHLTE DER AUFRUF! ⬇️
initDragAndDrop();
}
});
@ -104,11 +134,13 @@ function createNewSection() {
*/
function moveSection(sectionId, direction) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
debugLog('🔄 Verschiebe Section ' + sectionId + ' ' + direction);
var docInfo = getDocumentInfo();
debugLog('🔄 Verschiebe Section ' + sectionId + ' ' + direction + ' (docType: ' + docInfo.type + ')');
$.post('/dolibarr/custom/subtotaltitle/ajax/move_section.php', {
section_id: sectionId,
direction: direction
direction: direction,
document_type: docInfo.type
}, function(response) {
debugLog('Move response: ' + JSON.stringify(response));
if (response.success) {
@ -401,25 +433,56 @@ function saveCurrentOrder() {
else if ($row.attr('id') && $row.attr('id').indexOf('row-') === 0) {
var productId = $row.attr('id').replace('row-', '');
// Prüfe ob Produkt explizit einer Section zugeordnet ist oder frei ist
var rowParentSection = $row.attr('data-parent-section');
var assignToSection = null;
if (rowParentSection) {
// Produkt hat explizite Section-Zuordnung - behalten
assignToSection = rowParentSection;
} else if (currentSectionId) {
// Produkt hat keine Zuordnung - prüfe ob es NACH einem Subtotal steht
// Wenn ja, bleibt es frei (am Ende der Liste)
var $prevRows = $row.prevAll('tr.subtotal-row[data-section-id="' + currentSectionId + '"]');
if ($prevRows.length === 0) {
// Kein Subtotal davor = Produkt gehört zur aktuellen Section
assignToSection = currentSectionId;
}
// Sonst: Subtotal davor = Produkt ist frei (nach der Section)
}
updates.push({
type: 'product',
id: productId,
order: order,
parent_section: currentSectionId
parent_section: assignToSection
});
debugLog(' ' + order + '. 📦 Produkt #' + productId + ' → ' + (currentSectionId ? 'Section ' + currentSectionId : 'FREI'));
debugLog(' ' + order + '. 📦 Produkt #' + productId + ' → ' + (assignToSection ? 'Section ' + assignToSection : 'FREI'));
order++;
}
else if ($row.hasClass('textline-row')) {
var textlineId = $row.attr('data-textline-id');
if (textlineId) {
// Gleiche Logik wie für Produkte
var textParentSection = $row.attr('data-parent-section');
var textAssignToSection = null;
if (textParentSection) {
textAssignToSection = textParentSection;
} else if (currentSectionId) {
var $prevSubtotals = $row.prevAll('tr.subtotal-row[data-section-id="' + currentSectionId + '"]');
if ($prevSubtotals.length === 0) {
textAssignToSection = currentSectionId;
}
}
updates.push({
type: 'text',
id: textlineId,
order: order,
parent_section: currentSectionId
parent_section: textAssignToSection
});
debugLog(' ' + order + '. 📝 Text #' + textlineId + ' → ' + (currentSectionId ? 'Section ' + currentSectionId : 'FREI'));
debugLog(' ' + order + '. 📝 Text #' + textlineId + ' → ' + (textAssignToSection ? 'Section ' + textAssignToSection : 'FREI'));
order++;
}
}
@ -697,59 +760,67 @@ function initCollapse() {
colorSections();
// NEU: Fehlende Subtotals einfügen
insertMissingSubtotals();
// DEAKTIVIERT: JavaScript-Subtotal verursacht Duplikate
// insertLastSectionSubtotal();
debugLog('✅ Collapse initialisiert');
}
/**
* Fügt fehlende Subtotals am Ende ein (für letzte Section)
* Prüft und zeigt Subtotal für die LETZTE Section an (wenn aktiviert)
* PHP rendert Subtotals nur zwischen Sections, nicht am Ende der Tabelle
* Diese Funktion holt die Daten via AJAX und fügt die Zeile mit Checkbox ein
*/
function insertMissingSubtotals() {
debugLog('🔢 Prüfe fehlende Subtotals...');
function insertLastSectionSubtotal() {
debugLog('🔢 Prüfe Subtotal für letzte Section...');
$('tr.section-header').each(function() {
var $header = $(this);
var sectionId = $header.attr('data-section-id');
var $checkbox = $header.find('.subtotal-toggle');
// Nur wenn Checkbox aktiviert ist
if (!$checkbox.length || !$checkbox.is(':checked')) {
var $allSections = $('tr.section-header');
if ($allSections.length === 0) {
debugLog(' Keine Sections vorhanden');
return;
}
// Finde letztes Produkt dieser Section
// Nur die LETZTE Section prüfen
var $lastHeader = $allSections.last();
var sectionId = $lastHeader.attr('data-section-id');
var $checkbox = $lastHeader.find('.subtotal-toggle');
// Nur wenn Checkbox existiert UND aktiviert ist
if (!$checkbox.length || !$checkbox.is(':checked')) {
debugLog(' Letzte Section ' + sectionId + ': Subtotal nicht aktiviert');
return;
}
// Finde Produkte dieser Section
var $products = $('tr[data-parent-section="' + sectionId + '"]');
if ($products.length === 0) {
debugLog(' Letzte Section ' + sectionId + ': Keine Produkte');
return;
}
var $lastProduct = $products.last();
// Prüfe ob direkt danach schon ein Subtotal kommt
var $nextRow = $lastProduct.next('tr');
if ($nextRow.hasClass('subtotal-row')) {
debugLog(' Section ' + sectionId + ': Subtotal vorhanden ✓');
// Prüfe ob Subtotal für diese Section irgendwo im DOM existiert
// (sowohl data-section-id als auch data-subtotal-id prüfen)
var $existingSubtotal = $('tr.subtotal-row[data-section-id="' + sectionId + '"]');
if ($existingSubtotal.length > 0) {
debugLog(' Letzte Section ' + sectionId + ': Subtotal existiert bereits im DOM ✓');
return;
}
debugLog(' Section ' + sectionId + ': Subtotal fehlt - füge ein...');
// Prüfe auch nächste Zeile nach letztem Produkt
var $nextRow = $lastProduct.next('tr');
if ($nextRow.hasClass('subtotal-row') || $nextRow.find('td:contains("Zwischensumme")').length > 0) {
debugLog(' Letzte Section ' + sectionId + ': Subtotal direkt nach Produkt ✓');
return;
}
// Berechne Summe aus Produktzeilen
debugLog(' Letzte Section ' + sectionId + ': Subtotal fehlt in DOM, hole Daten...');
// Berechne Summe lokal
var sum = 0;
$products.each(function() {
var $row = $(this);
// Versuche Netto-Betrag aus der Zeile zu holen
var priceText = $row.find('td.linecolht').text().trim();
if (!priceText) {
// Fallback: letzte Spalte mit Zahl
$row.find('td').each(function() {
var text = $(this).text().trim();
if (text.match(/^-?[\d\s.,]+$/)) {
priceText = text;
}
});
}
var priceText = $(this).find('td.linecolht').text().trim();
if (priceText) {
var price = parseFloat(priceText.replace(/\s/g, '').replace('.', '').replace(',', '.'));
if (!isNaN(price)) {
@ -763,19 +834,66 @@ function insertMissingSubtotals() {
maximumFractionDigits: 2
});
// Subtotal-Zeile einfügen
var html = '<tr class="subtotal-row" data-section-id="' + sectionId + '" style="font-weight:bold;">';
html += '<td colspan="9" style="text-align:right; padding:8px;">Zwischensumme:</td>';
html += '<td class="linecolht right" style="padding:8px;">' + formattedSum + '</td>';
// Hole Subtotal-ID aus Datenbank (oder erstelle ihn falls nötig)
var docType = getDocumentType();
$.ajax({
url: '/dolibarr/custom/subtotaltitle/ajax/check_subtotal.php',
method: 'GET',
data: {
section_id: sectionId,
document_type: docType
},
dataType: 'json',
success: function(response) {
if (response.exists && response.subtotal_id) {
// Subtotal existiert - zeige ihn mit Checkbox an
renderSubtotalRow($lastProduct, sectionId, response.subtotal_id, response.in_facturedet, formattedSum);
} else {
// Subtotal existiert nicht in DB - zeige ohne Checkbox (nur Vorschau)
renderSubtotalRow($lastProduct, sectionId, null, false, formattedSum);
}
},
error: function() {
// Bei Fehler: zeige Subtotal ohne Checkbox
renderSubtotalRow($lastProduct, sectionId, null, false, formattedSum);
}
});
}
/**
* Rendert eine Subtotal-Zeile im DOM
*/
function renderSubtotalRow($afterElement, sectionId, subtotalId, inFacturedet, formattedSum) {
var inClass = inFacturedet ? ' in-facturedet' : '';
var syncChecked = inFacturedet ? 'checked' : '';
var html = '<tr class="subtotal-row' + inClass + '" data-section-id="' + sectionId + '"';
if (subtotalId) {
html += ' data-subtotal-id="' + subtotalId + '"';
}
html += ' style="font-weight:bold; background:#f5f5f5;">';
html += '<td colspan="9" style="text-align:right; padding:8px;">';
html += 'Zwischensumme:';
// Checkbox nur wenn Subtotal in DB existiert
if (subtotalId) {
html += ' <label style="font-weight:normal;font-size:12px;margin-left:10px;" title="In Rechnung/PDF anzeigen">';
html += '<input type="checkbox" class="sync-checkbox" data-line-id="' + subtotalId + '" data-line-type="subtotal" ' + syncChecked;
html += ' onclick="toggleFacturedetSync(' + subtotalId + ', \'subtotal\', this);">';
html += ' 📄</label>';
}
html += '</td>';
html += '<td class="linecolht right" style="padding:8px;">' + formattedSum + ' €</td>';
html += '<td class="linecoledit"></td>';
html += '<td class="linecoldelete"></td>';
html += '<td class="linecolmove"></td>';
html += '<td class="linecolunlink"></td>';
html += '</tr>';
$lastProduct.after(html);
debugLog(' → Subtotal eingefügt: ' + formattedSum + ' €');
});
$afterElement.after(html);
debugLog(' → Subtotal eingefügt: ' + formattedSum + ' € (ID: ' + (subtotalId || 'keine') + ')');
}
$(document).ready(function() {
@ -816,24 +934,26 @@ function insertTextLines() {
}
function toggleSubtotal(sectionId, checkbox) {
event.stopPropagation();
if (event) event.stopPropagation();
var show = checkbox.checked;
var docType = getDocumentType();
debugLog('🔢 Toggle Subtotal für Section ' + sectionId + ': ' + show);
debugLog('🔢 Toggle Subtotal für Section ' + sectionId + ': ' + show + ', docType: ' + docType);
$.ajax({
url: '/dolibarr/custom/subtotaltitle/ajax/toggle_subtotal.php',
method: 'POST',
data: {
section_id: sectionId,
show: show ? 1 : 0
show: show ? 1 : 0,
document_type: docType
},
dataType: 'json',
success: function(response) {
debugLog('Subtotal Response: ' + JSON.stringify(response));
if (response.success && response.reload) {
window.location.reload();
safeReload();
}
},
error: function(xhr, status, error) {
@ -1032,14 +1152,16 @@ function removeFromSection(productId) {
return;
}
debugLog('🔓 Entferne Produkt ' + productId + ' aus Section');
var docType = getDocumentType();
debugLog('🔓 Entferne Produkt ' + productId + ' aus Section (docType: ' + docType + ')');
$.post('/dolibarr/custom/subtotaltitle/ajax/remove_from_section.php', {
product_id: productId
product_id: productId,
document_type: docType
}, function(response) {
debugLog('Remove response: ' + JSON.stringify(response));
if (response.success) {
window.location.href = window.location.pathname + window.location.search;
safeReload();
} else {
alert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
@ -1114,7 +1236,7 @@ function deleteMassSelected() {
facture_id: getFactureId()
}, function(response) {
if (response.success) {
window.location.reload();
safeReload();
} else {
alert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannt'));
}

View file

@ -135,6 +135,8 @@ function syncAllToFacturedet() {
return;
}
var docType = getDocumentTypeForSync();
$unchecked.each(function() {
var lineId = $(this).data('line-id');
var lineType = $(this).data('line-type');
@ -142,7 +144,8 @@ function syncAllToFacturedet() {
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
action: 'add',
line_id: lineId,
line_type: lineType
line_type: lineType,
document_type: docType
}, function(response) {
done++;
if (response.success) {
@ -189,6 +192,8 @@ function removeAllFromFacturedet() {
return;
}
var docType = getDocumentTypeForSync();
$checked.each(function() {
var lineId = $(this).data('line-id');
var lineType = $(this).data('line-type');
@ -196,7 +201,8 @@ function removeAllFromFacturedet() {
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
action: 'remove',
line_id: lineId,
line_type: lineType
line_type: lineType,
document_type: docType
}, function(response) {
done++;
if (response.success) {
@ -237,12 +243,15 @@ function updateAllSubtotals() {
return;
}
var docType = getDocumentTypeForSync();
$subtotals.each(function() {
var lineId = $(this).data('line-id');
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
action: 'update_subtotal',
line_id: lineId
line_id: lineId,
document_type: docType
}, function(response) {
done++;
debugLog('Subtotal #' + lineId + ' updated: ' + JSON.stringify(response));