Fehlerbehebungen Drag and Drop, Checkboxen, Anzeige Zwischensumme. Fehler in New Order fix_sections
This commit is contained in:
parent
050f2316b2
commit
954329d701
12 changed files with 718 additions and 183 deletions
|
|
@ -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
48
ajax/check_subtotal.php
Normal 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
144
ajax/cleanup_subtotals.php
Normal 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
|
||||
]);
|
||||
|
|
@ -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
58
ajax/fix_sections.php
Normal 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'
|
||||
]);
|
||||
|
|
@ -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)."'";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
$sql_upd .= " SET rang = ".$rang;
|
||||
$sql_upd .= " WHERE rowid = ".(int)$obj->fk_facturedet;
|
||||
$db->query($sql_upd);
|
||||
$rang++;
|
||||
$detail_id = $obj->detail_id;
|
||||
if ($detail_id) {
|
||||
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
|
||||
$sql_upd .= " SET rang = ".$rang;
|
||||
$sql_upd .= " WHERE rowid = ".(int)$detail_id;
|
||||
$db->query($sql_upd);
|
||||
$rang++;
|
||||
}
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
|
|
|||
|
|
@ -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()]);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,23 +596,31 @@ 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) {
|
||||
$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)$last_parent_section[$doc_key];
|
||||
$sql_subtotal .= " AND line_type = 'subtotal'";
|
||||
$resql_subtotal = $db->query($sql_subtotal);
|
||||
// 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 ($obj_sub = $db->fetch_object($resql_subtotal)) {
|
||||
$subtotal_key = 'subtotal_'.$obj_sub->rowid;
|
||||
if (!in_array($subtotal_key, self::$rendered_sections[$doc_key])) {
|
||||
echo $this->renderSubtotalLine($obj_sub);
|
||||
self::$rendered_sections[$doc_key][] = $subtotal_key;
|
||||
if ($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)$last_parent_section[$doc_key];
|
||||
$sql_subtotal .= " AND line_type = 'subtotal'";
|
||||
$resql_subtotal = $db->query($sql_subtotal);
|
||||
|
||||
if ($this->debug) {
|
||||
error_log('[SubtotalTitle] ✅ Subtotal "'.$obj_sub->title.'" gerendert (Section-Wechsel)');
|
||||
if ($obj_sub = $db->fetch_object($resql_subtotal)) {
|
||||
$subtotal_key = 'subtotal_'.$obj_sub->rowid;
|
||||
if (!in_array($subtotal_key, self::$rendered_sections[$doc_key])) {
|
||||
echo $this->renderSubtotalLine($obj_sub);
|
||||
self::$rendered_sections[$doc_key][] = $subtotal_key;
|
||||
|
||||
if ($this->debug) {
|
||||
error_log('[SubtotalTitle] ✅ Subtotal "'.$obj_sub->title.'" gerendert (Section-Wechsel)');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,29 +894,40 @@ 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) {
|
||||
$next_order = $this->getNextLineOrder($document_id, $docType);
|
||||
// Neues Produkt gefunden - merken mit rang
|
||||
$new_products[] = array('rowid' => $obj->rowid, 'rang' => $obj->rang);
|
||||
}
|
||||
}
|
||||
|
||||
// Setze alle FK-Felder explizit (NULL für nicht genutzte)
|
||||
$fk_facture = ($docType === 'invoice') ? (int)$document_id : 'NULL';
|
||||
$fk_propal = ($docType === 'propal') ? (int)$document_id : 'NULL';
|
||||
$fk_commande = ($docType === 'order') ? (int)$document_id : 'NULL';
|
||||
// 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)
|
||||
$fk_facture = ($docType === 'invoice') ? (int)$document_id : 'NULL';
|
||||
$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;
|
||||
|
|
|
|||
|
|
@ -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,85 +760,140 @@ 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');
|
||||
var $allSections = $('tr.section-header');
|
||||
if ($allSections.length === 0) {
|
||||
debugLog(' Keine Sections vorhanden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Nur wenn Checkbox aktiviert ist
|
||||
if (!$checkbox.length || !$checkbox.is(':checked')) {
|
||||
return;
|
||||
}
|
||||
// Nur die LETZTE Section prüfen
|
||||
var $lastHeader = $allSections.last();
|
||||
var sectionId = $lastHeader.attr('data-section-id');
|
||||
var $checkbox = $lastHeader.find('.subtotal-toggle');
|
||||
|
||||
// Finde letztes Produkt dieser Section
|
||||
var $products = $('tr[data-parent-section="' + sectionId + '"]');
|
||||
if ($products.length === 0) {
|
||||
return;
|
||||
}
|
||||
// Nur wenn Checkbox existiert UND aktiviert ist
|
||||
if (!$checkbox.length || !$checkbox.is(':checked')) {
|
||||
debugLog(' Letzte Section ' + sectionId + ': Subtotal nicht aktiviert');
|
||||
return;
|
||||
}
|
||||
|
||||
var $lastProduct = $products.last();
|
||||
// Finde Produkte dieser Section
|
||||
var $products = $('tr[data-parent-section="' + sectionId + '"]');
|
||||
if ($products.length === 0) {
|
||||
debugLog(' Letzte Section ' + sectionId + ': Keine Produkte');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe ob direkt danach schon ein Subtotal kommt
|
||||
var $nextRow = $lastProduct.next('tr');
|
||||
if ($nextRow.hasClass('subtotal-row')) {
|
||||
debugLog(' Section ' + sectionId + ': Subtotal vorhanden ✓');
|
||||
return;
|
||||
}
|
||||
var $lastProduct = $products.last();
|
||||
|
||||
debugLog(' Section ' + sectionId + ': Subtotal fehlt - füge ein...');
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Berechne Summe aus Produktzeilen
|
||||
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;
|
||||
}
|
||||
});
|
||||
// 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;
|
||||
}
|
||||
|
||||
debugLog(' Letzte Section ' + sectionId + ': Subtotal fehlt in DOM, hole Daten...');
|
||||
|
||||
// Berechne Summe lokal
|
||||
var sum = 0;
|
||||
$products.each(function() {
|
||||
var priceText = $(this).find('td.linecolht').text().trim();
|
||||
if (priceText) {
|
||||
var price = parseFloat(priceText.replace(/\s/g, '').replace('.', '').replace(',', '.'));
|
||||
if (!isNaN(price)) {
|
||||
sum += price;
|
||||
}
|
||||
if (priceText) {
|
||||
var price = parseFloat(priceText.replace(/\s/g, '').replace('.', '').replace(',', '.'));
|
||||
if (!isNaN(price)) {
|
||||
sum += price;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var formattedSum = sum.toLocaleString('de-DE', {
|
||||
minimumFractionDigits: 2,
|
||||
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>';
|
||||
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 + ' €');
|
||||
}
|
||||
});
|
||||
|
||||
var formattedSum = sum.toLocaleString('de-DE', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
|
||||
// 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>';
|
||||
|
||||
$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'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Reference in a new issue