diff --git a/ajax/import_from_origin.php b/ajax/import_from_origin.php new file mode 100644 index 0000000..1e93dda --- /dev/null +++ b/ajax/import_from_origin.php @@ -0,0 +1,565 @@ + + * + * Import sections/textlines from origin document (Angebot→Auftrag→Rechnung) + */ + +define('NOTOKENRENEWAL', 1); + +$res = 0; +if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php"; +if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php"; +if (!$res) die("Include of main fails"); + +require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php'; +require_once DOL_DOCUMENT_ROOT.'/comm/propal/class/propal.class.php'; +require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php'; +dol_include_once('/subtotaltitle/lib/subtotaltitle.lib.php'); +require_once __DIR__.'/../class/DocumentTypeHelper.class.php'; + +header('Content-Type: application/json'); + +$action = GETPOST('action', 'alpha'); +$target_id = GETPOST('target_id', 'int'); +$target_type = GETPOST('target_type', 'alpha'); + +subtotaltitle_debug_log('📥 import_from_origin: action='.$action.', target_id='.$target_id.', target_type='.$target_type); + +if (!$target_id || !$target_type) { + echo json_encode(array('success' => false, 'error' => 'Missing parameters')); + exit; +} + +// Hole die richtigen Tabellennamen für Ziel-Dokumenttyp +$target_tables = DocumentTypeHelper::getTableNames($target_type); +if (!$target_tables) { + echo json_encode(array('success' => false, 'error' => 'Invalid target document type')); + exit; +} + +/** + * Ermittelt das Ursprungsdokument basierend auf Zieldokument + * Dolibarr speichert die Herkunft in origin/origin_id ODER in llx_element_element + */ +function getOriginDocument($db, $target_id, $target_type) +{ + $target_tables = DocumentTypeHelper::getTableNames($target_type); + if (!$target_tables) { + return null; + } + + // Lade Zieldokument + $target_doc = DocumentTypeHelper::loadDocument($target_type, $target_id, $db); + if (!$target_doc) { + subtotaltitle_debug_log('❌ Zieldokument nicht gefunden: '.$target_type.' #'.$target_id); + return null; + } + + subtotaltitle_debug_log('🔍 Zieldokument geladen: element='.$target_doc->element.', origin='.($target_doc->origin ?? 'NULL').', origin_id='.($target_doc->origin_id ?? 'NULL')); + + // Methode 1: Direkte Objekteigenschaften prüfen + $origin = $target_doc->origin ?? null; + $origin_id = $target_doc->origin_id ?? null; + + // Methode 2: Falls nicht gesetzt, prüfe llx_element_element Tabelle + if (empty($origin) || empty($origin_id)) { + $elementType = $target_doc->element; // z.B. 'commande', 'facture', 'propal' + subtotaltitle_debug_log('🔍 Suche in element_element für '.$elementType.' #'.$target_id); + + $sql_origin = "SELECT fk_source, sourcetype FROM ".MAIN_DB_PREFIX."element_element"; + $sql_origin .= " WHERE fk_target = ".(int)$target_id; + $sql_origin .= " AND targettype = '".$db->escape($elementType)."'"; + $sql_origin .= " LIMIT 1"; + + subtotaltitle_debug_log('SQL: '.$sql_origin); + + $res_origin = $db->query($sql_origin); + if ($res_origin && $db->num_rows($res_origin) > 0) { + $obj_origin = $db->fetch_object($res_origin); + $origin = $obj_origin->sourcetype; + $origin_id = $obj_origin->fk_source; + subtotaltitle_debug_log('✅ Gefunden in element_element: '.$origin.' #'.$origin_id); + } else { + subtotaltitle_debug_log('❌ Kein Eintrag in element_element gefunden'); + } + } + + // Prüfe ob origin gesetzt ist + if (empty($origin) || empty($origin_id)) { + subtotaltitle_debug_log('❌ Kein Ursprungsdokument verknüpft (weder direkt noch in element_element)'); + return null; + } + + // Mappe Dolibarr origin zu unserem document_type + $origin_type_map = array( + 'propal' => 'propal', + 'commande' => 'order', + 'facture' => 'invoice', + 'order_supplier' => null, // Lieferantenauftrag - nicht unterstützt + 'invoice_supplier' => null // Lieferantenrechnung - nicht unterstützt + ); + + $origin_type = isset($origin_type_map[$origin]) ? $origin_type_map[$origin] : null; + if (!$origin_type) { + subtotaltitle_debug_log('❌ Nicht unterstützter Ursprungstyp: '.$origin); + return null; + } + + // Lade Ursprungsdokument + $origin_doc = DocumentTypeHelper::loadDocument($origin_type, $origin_id, $db); + if (!$origin_doc) { + subtotaltitle_debug_log('❌ Ursprungsdokument nicht gefunden: '.$origin_type.' #'.$origin_id); + return null; + } + + subtotaltitle_debug_log('✅ Ursprungsdokument gefunden: '.$origin_type.' #'.$origin_id); + + return array( + 'document' => $origin_doc, + 'type' => $origin_type, + 'id' => $origin_id + ); +} + +/** + * Sucht die passende Produktzeile im Zieldokument basierend auf fk_product + */ +function findMatchingProductLine($db, $target_id, $target_type, $source_product_id) +{ + if (!$source_product_id) { + return null; + } + + $target_tables = DocumentTypeHelper::getTableNames($target_type); + + // Suche nach Zeile mit gleichem Produkt im Zieldokument + $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX.$target_tables['lines_table']; + $sql .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id; + $sql .= " AND fk_product = ".(int)$source_product_id; + $sql .= " LIMIT 1"; + + $resql = $db->query($sql); + if ($resql && $db->num_rows($resql) > 0) { + $obj = $db->fetch_object($resql); + return $obj->rowid; + } + + return null; +} + +if ($action == 'check') { + // ========== PRÜFE OB IMPORT MÖGLICH IST ========== + + $origin = getOriginDocument($db, $target_id, $target_type); + if (!$origin) { + echo json_encode(array( + 'success' => true, + 'has_origin' => false, + 'message' => 'Kein Ursprungsdokument verknüpft' + )); + exit; + } + + $origin_tables = DocumentTypeHelper::getTableNames($origin['type']); + + // Zähle Sections und Textlines im Ursprungsdokument + $sql = "SELECT COUNT(*) as cnt, line_type FROM ".MAIN_DB_PREFIX."facture_lines_manager"; + $sql .= " WHERE ".$origin_tables['fk_parent']." = ".(int)$origin['id']; + $sql .= " AND document_type = '".$db->escape($origin['type'])."'"; + $sql .= " AND line_type IN ('section', 'text')"; + $sql .= " GROUP BY line_type"; + + $resql = $db->query($sql); + $counts = array('section' => 0, 'text' => 0); + while ($obj = $db->fetch_object($resql)) { + $counts[$obj->line_type] = $obj->cnt; + } + + // Prüfe ob im Zieldokument schon Sections existieren + $sql_target = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."facture_lines_manager"; + $sql_target .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id; + $sql_target .= " AND document_type = '".$db->escape($target_type)."'"; + $sql_target .= " AND line_type = 'section'"; + $res_target = $db->query($sql_target); + $obj_target = $db->fetch_object($res_target); + $has_existing = ($obj_target && $obj_target->cnt > 0); + + // Ermittle Anzeigename für Ursprungsdokument + $origin_name = ''; + $origin_ref = $origin['document']->ref; + switch ($origin['type']) { + case 'propal': + $origin_name = 'Angebot '.$origin_ref; + break; + case 'order': + $origin_name = 'Auftrag '.$origin_ref; + break; + case 'invoice': + $origin_name = 'Rechnung '.$origin_ref; + break; + } + + echo json_encode(array( + 'success' => true, + 'has_origin' => true, + 'origin_type' => $origin['type'], + 'origin_id' => $origin['id'], + 'origin_ref' => $origin_ref, + 'origin_name' => $origin_name, + 'sections_count' => (int)$counts['section'], + 'textlines_count' => (int)$counts['text'], + 'has_existing' => $has_existing, + 'can_import' => ($counts['section'] > 0 || $counts['text'] > 0) + )); + +} elseif ($action == 'import') { + // ========== KOMPLETTER IMPORT MIT RANG-SYNCHRONISATION ========== + // Strategie: + // 1. Lösche bestehende Einträge in Manager-Tabelle für Zieldokument + // 2. Importiere alle Sections/Textlines/Subtotals aus Ursprung + // 3. Synchronisiere ALLE Produkte aus Ziel-Dolibarr-Tabelle in Manager-Tabelle + // 4. Ordne Produkte den Sections zu basierend auf fk_product Matching + // 5. Produkte die NUR im Zieldokument sind, kommen am Ende + // 6. Neu-Nummerierung line_order UND rang in beiden Tabellen + + $origin = getOriginDocument($db, $target_id, $target_type); + if (!$origin) { + echo json_encode(array('success' => false, 'error' => 'Kein Ursprungsdokument gefunden')); + exit; + } + + $origin_tables = DocumentTypeHelper::getTableNames($origin['type']); + + // Starte Transaktion + $db->begin(); + + $imported_sections = 0; + $imported_textlines = 0; + $imported_subtotals = 0; + $product_assignments = 0; + $new_products = 0; + $section_mapping = array(); // Alte Section-ID => Neue Section-ID + + try { + // ============================================================ + // SCHRITT 1: Lösche bestehende Einträge in Manager-Tabelle + // ============================================================ + subtotaltitle_debug_log('🗑️ Lösche bestehende Manager-Einträge...'); + + $sql_delete = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager"; + $sql_delete .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id; + $sql_delete .= " AND document_type = '".$db->escape($target_type)."'"; + $db->query($sql_delete); + + subtotaltitle_debug_log('✅ Bestehende Einträge gelöscht'); + + // FK-Werte für Zieldokument + $fk_facture = ($target_type === 'invoice') ? (int)$target_id : 'NULL'; + $fk_propal = ($target_type === 'propal') ? (int)$target_id : 'NULL'; + $fk_commande = ($target_type === 'order') ? (int)$target_id : 'NULL'; + + // ============================================================ + // SCHRITT 2: Baue Mapping fk_product → Section aus Ursprung + // ============================================================ + subtotaltitle_debug_log('🗺️ Erstelle fk_product → Section Mapping...'); + + $product_section_map = array(); // fk_product => origin_section_id + + $sql_origin_products = "SELECT m.parent_section, d.fk_product FROM ".MAIN_DB_PREFIX."facture_lines_manager m"; + $sql_origin_products .= " LEFT JOIN ".MAIN_DB_PREFIX.$origin_tables['lines_table']." d ON d.rowid = m.".$origin_tables['fk_line']; + $sql_origin_products .= " WHERE m.".$origin_tables['fk_parent']." = ".(int)$origin['id']; + $sql_origin_products .= " AND m.document_type = '".$db->escape($origin['type'])."'"; + $sql_origin_products .= " AND m.line_type = 'product'"; + $sql_origin_products .= " AND m.parent_section IS NOT NULL"; + $sql_origin_products .= " AND d.fk_product IS NOT NULL"; + + $res_origin_products = $db->query($sql_origin_products); + while ($row = $db->fetch_object($res_origin_products)) { + $product_section_map[$row->fk_product] = $row->parent_section; + subtotaltitle_debug_log(' Mapping: fk_product='.$row->fk_product.' → Section #'.$row->parent_section); + } + + subtotaltitle_debug_log('✅ '.count($product_section_map).' Produkt-Section Mappings erstellt'); + + // ============================================================ + // SCHRITT 3: Hole ALLE Einträge aus Ursprung (sortiert nach line_order) + // ============================================================ + subtotaltitle_debug_log('📦 Hole alle Einträge aus Ursprungsdokument...'); + + $sql_origin_all = "SELECT m.*, d.fk_product FROM ".MAIN_DB_PREFIX."facture_lines_manager m"; + $sql_origin_all .= " LEFT JOIN ".MAIN_DB_PREFIX.$origin_tables['lines_table']." d ON d.rowid = m.".$origin_tables['fk_line']; + $sql_origin_all .= " WHERE m.".$origin_tables['fk_parent']." = ".(int)$origin['id']; + $sql_origin_all .= " AND m.document_type = '".$db->escape($origin['type'])."'"; + $sql_origin_all .= " ORDER BY m.line_order"; + + $res_origin_all = $db->query($sql_origin_all); + + // Sammle alle Einträge gruppiert + $origin_entries = array(); + while ($entry = $db->fetch_object($res_origin_all)) { + $origin_entries[] = $entry; + } + + subtotaltitle_debug_log('✅ '.count($origin_entries).' Einträge aus Ursprung geladen'); + + // ============================================================ + // SCHRITT 4: Hole ALLE Produktzeilen aus Zieldokument (Dolibarr-Tabelle) + // ============================================================ + subtotaltitle_debug_log('📦 Hole alle Produkte aus Zieldokument...'); + + $sql_target_products = "SELECT rowid, fk_product, rang FROM ".MAIN_DB_PREFIX.$target_tables['lines_table']; + $sql_target_products .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id; + $sql_target_products .= " ORDER BY rang"; + + $res_target_products = $db->query($sql_target_products); + $target_products = array(); + while ($row = $db->fetch_object($res_target_products)) { + $target_products[$row->rowid] = $row; + } + + subtotaltitle_debug_log('✅ '.count($target_products).' Produkte aus Zieldokument geladen'); + + // Sammle fk_products die schon zugeordnet werden (aus Ursprung) + $assigned_fk_products = array(); + $assigned_line_ids = array(); + + // ============================================================ + // SCHRITT 5: Importiere Structure aus Ursprung mit richtiger Reihenfolge + // ============================================================ + subtotaltitle_debug_log('🏗️ Importiere Struktur aus Ursprungsdokument...'); + + $line_order = 10; + $rang = 1; + $new_entries = array(); // Sammle alle neuen Einträge für spätere Rang-Zuweisung + + foreach ($origin_entries as $entry) { + if ($entry->line_type === 'section') { + // Section importieren + $sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager"; + $sql_insert .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title,"; + $sql_insert .= " parent_section, show_subtotal, collapsed, line_order, in_facturedet, date_creation)"; + $sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.","; + $sql_insert .= " '".$db->escape($target_type)."', 'section',"; + $sql_insert .= " '".$db->escape($entry->title)."',"; + $sql_insert .= " NULL,"; + $sql_insert .= " ".(int)$entry->show_subtotal.","; + $sql_insert .= " ".(int)$entry->collapsed.","; + $sql_insert .= " ".(int)$line_order.","; + $sql_insert .= " 0, NOW())"; + + if (!$db->query($sql_insert)) { + throw new Exception('Fehler beim Erstellen der Section: '.$db->lasterror()); + } + + $new_section_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager"); + $section_mapping[$entry->rowid] = $new_section_id; + $imported_sections++; + $line_order += 10; + + subtotaltitle_debug_log('✅ Section: "'.$entry->title.'" (#'.$entry->rowid.' → #'.$new_section_id.')'); + + } elseif ($entry->line_type === 'text') { + // Textline importieren + $new_parent = isset($section_mapping[$entry->parent_section]) + ? (int)$section_mapping[$entry->parent_section] + : 'NULL'; + + $sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager"; + $sql_insert .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title,"; + $sql_insert .= " parent_section, line_order, in_facturedet, date_creation)"; + $sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.","; + $sql_insert .= " '".$db->escape($target_type)."', 'text',"; + $sql_insert .= " '".$db->escape($entry->title)."',"; + $sql_insert .= " ".$new_parent.","; + $sql_insert .= " ".(int)$line_order.","; + $sql_insert .= " 0, NOW())"; + + if (!$db->query($sql_insert)) { + throw new Exception('Fehler beim Erstellen der Textline: '.$db->lasterror()); + } + + $imported_textlines++; + $line_order += 10; + + subtotaltitle_debug_log('✅ Textline: "'.$entry->title.'"'); + + } elseif ($entry->line_type === 'subtotal') { + // Subtotal importieren (nur wenn parent Section existiert) + if (!isset($section_mapping[$entry->parent_section])) { + continue; + } + + $new_parent = (int)$section_mapping[$entry->parent_section]; + + $sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager"; + $sql_insert .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title,"; + $sql_insert .= " parent_section, line_order, in_facturedet, date_creation)"; + $sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.","; + $sql_insert .= " '".$db->escape($target_type)."', 'subtotal',"; + $sql_insert .= " '".$db->escape($entry->title)."',"; + $sql_insert .= " ".$new_parent.","; + $sql_insert .= " ".(int)$line_order.","; + $sql_insert .= " 0, NOW())"; + + if (!$db->query($sql_insert)) { + throw new Exception('Fehler beim Erstellen des Subtotals: '.$db->lasterror()); + } + + $imported_subtotals++; + $line_order += 10; + + subtotaltitle_debug_log('✅ Subtotal für Section #'.$new_parent); + + } elseif ($entry->line_type === 'product' && !empty($entry->fk_product)) { + // Produkt - finde passende Zeile im Zieldokument + $target_line_id = null; + + foreach ($target_products as $tp_id => $tp) { + if ($tp->fk_product == $entry->fk_product && !isset($assigned_line_ids[$tp_id])) { + $target_line_id = $tp_id; + $assigned_line_ids[$tp_id] = true; + $assigned_fk_products[$entry->fk_product] = true; + break; + } + } + + if ($target_line_id) { + $new_parent = isset($section_mapping[$entry->parent_section]) + ? (int)$section_mapping[$entry->parent_section] + : 'NULL'; + + $sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager"; + $sql_insert .= " (fk_facture, fk_propal, fk_commande, ".$target_tables['fk_line'].", document_type,"; + $sql_insert .= " line_type, parent_section, line_order, in_facturedet, date_creation)"; + $sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.","; + $sql_insert .= " ".(int)$target_line_id.", '".$db->escape($target_type)."',"; + $sql_insert .= " 'product', ".$new_parent.", ".(int)$line_order.", 1, NOW())"; + + if (!$db->query($sql_insert)) { + throw new Exception('Fehler beim Erstellen des Produkts: '.$db->lasterror()); + } + + // Speichere für Rang-Update + $new_entries[] = array( + 'type' => 'product', + 'line_id' => $target_line_id, + 'rang' => $rang + ); + + $product_assignments++; + $line_order += 10; + $rang++; + + subtotaltitle_debug_log('✅ Produkt: fk_product='.$entry->fk_product.' → Section #'.$new_parent.' (Line #'.$target_line_id.')'); + } + } + } + + // ============================================================ + // SCHRITT 6: Füge neue Produkte hinzu (nur im Zieldokument) + // ============================================================ + subtotaltitle_debug_log('➕ Füge neue Produkte hinzu (nur im Zieldokument)...'); + + foreach ($target_products as $tp_id => $tp) { + if (isset($assigned_line_ids[$tp_id])) { + continue; // Schon zugeordnet + } + + // Produkt ist NEU - füge am Ende hinzu (ohne Section) + $sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager"; + $sql_insert .= " (fk_facture, fk_propal, fk_commande, ".$target_tables['fk_line'].", document_type,"; + $sql_insert .= " line_type, parent_section, line_order, in_facturedet, date_creation)"; + $sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.","; + $sql_insert .= " ".(int)$tp_id.", '".$db->escape($target_type)."',"; + $sql_insert .= " 'product', NULL, ".(int)$line_order.", 1, NOW())"; + + if (!$db->query($sql_insert)) { + throw new Exception('Fehler beim Erstellen des neuen Produkts: '.$db->lasterror()); + } + + // Speichere für Rang-Update + $new_entries[] = array( + 'type' => 'product', + 'line_id' => $tp_id, + 'rang' => $rang + ); + + $new_products++; + $line_order += 10; + $rang++; + + subtotaltitle_debug_log('➕ Neues Produkt: Line #'.$tp_id.' (fk_product='.$tp->fk_product.')'); + } + + // ============================================================ + // SCHRITT 7: Finale Neu-Nummerierung in beiden Tabellen + // ============================================================ + subtotaltitle_debug_log('🔄 Finale Neu-Nummerierung in beiden Tabellen...'); + + // A) line_order in Manager-Tabelle (basierend auf tatsächlicher Reihenfolge) + $sql_reorder = "SELECT rowid, line_type, ".$target_tables['fk_line']." as fk_line FROM ".MAIN_DB_PREFIX."facture_lines_manager"; + $sql_reorder .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id; + $sql_reorder .= " AND document_type = '".$db->escape($target_type)."'"; + $sql_reorder .= " ORDER BY line_order"; + + $res_reorder = $db->query($sql_reorder); + + $final_order = 10; + $final_rang = 1; + $product_rang_updates = array(); + + while ($row = $db->fetch_object($res_reorder)) { + // Update line_order + $sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager SET line_order = ".(int)$final_order; + $sql_upd .= " WHERE rowid = ".(int)$row->rowid; + $db->query($sql_upd); + + // Sammle Rang für Produkte + if ($row->line_type === 'product' && $row->fk_line) { + $product_rang_updates[$row->fk_line] = $final_rang; + $final_rang++; + } + + $final_order += 10; + } + + subtotaltitle_debug_log('✅ line_order neu nummeriert'); + + // B) rang in Dolibarr-Tabelle aktualisieren + subtotaltitle_debug_log('🔄 Aktualisiere rang in Dolibarr-Tabelle ('.$target_tables['lines_table'].')...'); + + foreach ($product_rang_updates as $line_id => $new_rang) { + $sql_rang = "UPDATE ".MAIN_DB_PREFIX.$target_tables['lines_table']; + $sql_rang .= " SET rang = ".(int)$new_rang; + $sql_rang .= " WHERE rowid = ".(int)$line_id; + $db->query($sql_rang); + subtotaltitle_debug_log(' Line #'.$line_id.' → rang='.$new_rang); + } + + subtotaltitle_debug_log('✅ rang in Dolibarr-Tabelle aktualisiert ('.count($product_rang_updates).' Zeilen)'); + + // Commit Transaktion + $db->commit(); + + subtotaltitle_debug_log('✅ Import komplett: '.$imported_sections.' Sections, '.$imported_textlines.' Textlines, '.$imported_subtotals.' Subtotals, '.$product_assignments.' zugeordnete Produkte, '.$new_products.' neue Produkte'); + + echo json_encode(array( + 'success' => true, + 'imported_sections' => $imported_sections, + 'imported_textlines' => $imported_textlines, + 'imported_subtotals' => $imported_subtotals, + 'product_assignments' => $product_assignments, + 'new_products' => $new_products, + 'message' => sprintf('%d Sections, %d Textlines, %d Produkte zugeordnet, %d neue Produkte', + $imported_sections, $imported_textlines, $product_assignments, $new_products) + )); + + } catch (Exception $e) { + $db->rollback(); + subtotaltitle_debug_log('❌ Import fehlgeschlagen: '.$e->getMessage()); + echo json_encode(array('success' => false, 'error' => $e->getMessage())); + } + +} else { + echo json_encode(array('success' => false, 'error' => 'Unknown action')); +} diff --git a/ajax/sync_to_facturedet.php b/ajax/sync_to_facturedet.php index 36bba2b..2aae2db 100755 --- a/ajax/sync_to_facturedet.php +++ b/ajax/sync_to_facturedet.php @@ -312,6 +312,80 @@ if ($action == 'add') { echo json_encode(array('success' => true, 'total_ht' => $total_ht)); +} elseif ($action == 'remove_all') { + // ========== ALLE SPEZIALZEILEN UND VERWAISTE EINTRÄGE ENTFERNEN ========== + + // document_id wird benötigt + $document_id = GETPOST('document_id', 'int'); + if (!$document_id) { + echo json_encode(array('success' => false, 'error' => 'Missing document_id')); + exit; + } + + $removed_count = 0; + $orphan_count = 0; + + // 1. Entferne ALLE Einträge mit special_code 100, 101, 102 aus der Detail-Tabelle + // (unabhängig davon ob sie noch in der Manager-Tabelle existieren) + $sql_delete_all = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table']; + $sql_delete_all .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id; + $sql_delete_all .= " AND special_code IN (100, 101, 102)"; + + subtotaltitle_debug_log('🗑️ Remove ALL special lines: '.$sql_delete_all); + + if ($db->query($sql_delete_all)) { + $removed_count = $db->affected_rows($db->query("SELECT ROW_COUNT()")); + // Fallback: Zähle manuell wenn affected_rows nicht funktioniert + if ($removed_count === 0) { + // Zähle vorher + $sql_count = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX.$tables['lines_table']; + $sql_count .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id; + $sql_count .= " AND special_code IN (100, 101, 102)"; + // Da wir schon gelöscht haben, ist es jetzt 0 + $removed_count = -1; // Unbekannt, aber erfolgreich + } + } + + // 2. Setze in_facturedet und fk_*det auf NULL für alle Manager-Einträge dieses Dokuments + $sql_reset = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager"; + $sql_reset .= " SET ".$tables['fk_line']." = NULL, in_facturedet = 0"; + $sql_reset .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id; + $sql_reset .= " AND document_type = '".$db->escape($docType)."'"; + $sql_reset .= " AND line_type IN ('section', 'text', 'subtotal')"; + + subtotaltitle_debug_log('🔄 Reset manager entries: '.$sql_reset); + $db->query($sql_reset); + + // 3. Normalisiere die rang-Werte (schließe Lücken) + $sql_reorder = "SET @r = 0; UPDATE ".MAIN_DB_PREFIX.$tables['lines_table']; + $sql_reorder .= " SET rang = (@r := @r + 1)"; + $sql_reorder .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id; + $sql_reorder .= " ORDER BY rang"; + + // MySQL erlaubt kein SET in einer Anweisung mit UPDATE, also manuell: + $sql_get_lines = "SELECT rowid FROM ".MAIN_DB_PREFIX.$tables['lines_table']; + $sql_get_lines .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id; + $sql_get_lines .= " ORDER BY rang"; + $res_lines = $db->query($sql_get_lines); + + $new_rang = 1; + while ($obj = $db->fetch_object($res_lines)) { + $sql_upd_rang = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table']; + $sql_upd_rang .= " SET rang = ".(int)$new_rang; + $sql_upd_rang .= " WHERE rowid = ".(int)$obj->rowid; + $db->query($sql_upd_rang); + $new_rang++; + } + + subtotaltitle_debug_log('✅ Remove ALL completed: removed='.$removed_count); + + echo json_encode(array( + 'success' => true, + 'removed' => $removed_count, + 'orphans_cleaned' => $orphan_count, + 'message' => 'Alle Spezialzeilen wurden entfernt' + )); + } else { echo json_encode(array('success' => false, 'error' => 'Unknown action')); } diff --git a/class/actions_subtotaltitle.class.php b/class/actions_subtotaltitle.class.php index 8e95dd5..b1b0ed0 100755 --- a/class/actions_subtotaltitle.class.php +++ b/class/actions_subtotaltitle.class.php @@ -235,7 +235,15 @@ class ActionsSubtotalTitle extends CommonHookActions echo ' elementsRemovedWithErrors: '.json_encode($langs->trans('ElementsRemovedWithErrors')).','."\n"; echo ' successSyncedToInvoice: '.json_encode($langs->trans('SuccessSyncedToInvoice')).','."\n"; echo ' successRemovedFromInvoice: '.json_encode($langs->trans('SuccessRemovedFromInvoice')).','."\n"; - echo ' errorSyncing: '.json_encode($langs->trans('ErrorSyncing'))."\n"; + echo ' errorSyncing: '.json_encode($langs->trans('ErrorSyncing')).','."\n"; + // Import feature strings + echo ' importFromOrigin: '.json_encode($langs->trans('ImportFromOrigin')).','."\n"; + echo ' importFromOriginTitle: '.json_encode($langs->trans('ImportFromOriginTitle')).','."\n"; + echo ' importFromOriginConfirm: '.json_encode($langs->trans('ImportFromOriginConfirm')).','."\n"; + echo ' importFromOriginSuccess: '.json_encode($langs->trans('ImportFromOriginSuccess')).','."\n"; + echo ' importFromOriginNoOrigin: '.json_encode($langs->trans('ImportFromOriginNoOrigin')).','."\n"; + echo ' importFromOriginNoData: '.json_encode($langs->trans('ImportFromOriginNoData')).','."\n"; + echo ' importFromOriginError: '.json_encode($langs->trans('ImportFromOriginError'))."\n"; echo '};'."\n"; echo ''."\n"; @@ -247,6 +255,30 @@ class ActionsSubtotalTitle extends CommonHookActions $jsSyncPath = dol_buildpath('/custom/subtotaltitle/js/subtotaltitle_sync.js', 1); echo ''."\n"; + // Prüfe ob Dokument ein Ursprungsdokument hat (für Import-Feature) + // Methode 1: Direkte Objekteigenschaften + $hasOrigin = (!empty($object->origin) && !empty($object->origin_id)); + + // Methode 2: Falls nicht gesetzt, prüfe llx_element_element Tabelle + if (!$hasOrigin && $object->id > 0) { + $elementType = $object->element; // z.B. 'commande', 'facture', 'propal' + $sql_origin = "SELECT fk_source, sourcetype FROM ".MAIN_DB_PREFIX."element_element"; + $sql_origin .= " WHERE fk_target = ".(int)$object->id; + $sql_origin .= " AND targettype = '".$db->escape($elementType)."'"; + $sql_origin .= " LIMIT 1"; + $res_origin = $db->query($sql_origin); + if ($res_origin && $db->num_rows($res_origin) > 0) { + $obj_origin = $db->fetch_object($res_origin); + $object->origin = $obj_origin->sourcetype; + $object->origin_id = $obj_origin->fk_source; + $hasOrigin = true; + } + } + + // Debug-Log für Import-Feature + $logFile = '/srv/http/dolibarr/documents/subtotaltitle/debug.log'; + error_log('['.date('Y-m-d H:i:s').'] Import-Check - element: '.($object->element ?? 'NULL').', origin: '.($object->origin ?? 'NULL').', origin_id: '.($object->origin_id ?? 'NULL').', hasOrigin: '.($hasOrigin ? 'true' : 'false')."\n", 3, $logFile); + // Buttons nur im Entwurfsstatus anzeigen if ($is_draft) { // Textzeile-Button @@ -256,6 +288,16 @@ class ActionsSubtotalTitle extends CommonHookActions echo ' }'; echo '});'."\n"; + // Import-Button (wenn Ursprungsdokument existiert) + // Button erscheint immer - die check-Action prüft ob es etwas zu importieren gibt + if ($hasOrigin) { + echo ''."\n"; + } + // Massenlösch-Button (ans ENDE der Hauptzeile) - NUR EINMAL EINFÜGEN echo ''; diff --git a/core/modules/modSubtotalTitle.class.php b/core/modules/modSubtotalTitle.class.php index bf7088d..fdca161 100755 --- a/core/modules/modSubtotalTitle.class.php +++ b/core/modules/modSubtotalTitle.class.php @@ -76,7 +76,7 @@ class modSubtotalTitle extends DolibarrModules $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@subtotaltitle' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '4.0'; + $this->version = '4.1'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; diff --git a/js/subtotaltitle.js b/js/subtotaltitle.js index 23fc84d..fb1e1c9 100755 --- a/js/subtotaltitle.js +++ b/js/subtotaltitle.js @@ -345,8 +345,8 @@ function createNewSection() { } showInputDialog( - lang.sectionCreate || 'Positionsgruppe erstellen', - lang.sectionName || 'Name der Positionsgruppe:', + lang.sectionCreate || 'Produktgruppe erstellen', + lang.sectionName || 'Name der Produktgruppe:', '', function(title) { debugLog('Erstelle Section: ' + title + ' für ' + docInfo.type + ' ID ' + docInfo.id); @@ -414,8 +414,8 @@ function renameSection(sectionId, currentTitle) { var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; showInputDialog( - lang.buttonEdit || 'Positionsgruppe umbenennen', - lang.sectionName || 'Name der Positionsgruppe:', + lang.sectionEdit || 'Produktgruppe umbenennen', + lang.sectionName || 'Name der Produktgruppe:', currentTitle || '', function(newTitle) { debugLog('✏️ Benenne Section ' + sectionId + ' um zu: ' + newTitle); @@ -820,15 +820,42 @@ function moveProductToSection(productId, sectionId, newLineOrder) { function addUnlinkColumn() { var $table = $('#tablelines'); if (!$table.length) return; - + + // NUR auf Dokumentdetailseiten ausführen (card), NICHT auf Listen + // Prüfe ob wir auf einer gültigen Seite sind (invoicecard, propalcard, ordercard) + var url = window.location.href; + var isDocumentCard = (url.indexOf('/facture/card.php') !== -1 || + url.indexOf('/propal/card.php') !== -1 || + url.indexOf('/commande/card.php') !== -1); + + // Zusätzliche Prüfung: Modul muss aktiv sein (subtotalTitleIsDraft wird von PHP gesetzt) + var isModuleActive = (typeof subtotalTitleIsDraft !== 'undefined'); + + if (!isDocumentCard) { + debugLog('🔗 Keine Dokumentdetailseite, Unlink-Spalte wird übersprungen'); + return; + } + + if (!isModuleActive) { + debugLog('🔗 SubtotalTitle Modul nicht aktiv auf dieser Seite, Unlink-Spalte wird übersprungen'); + return; + } + + // Prüfe ob mindestens eine Produktgruppe (Section) vorhanden ist + var hasSections = ($('tr.section-header').length > 0); + if (!hasSections) { + debugLog('🔗 Keine Produktgruppen vorhanden, Unlink-Spalte wird übersprungen'); + return; + } + // Prüfe ob schon ausgeführt if ($table.data('unlink-added')) { debugLog('🔗 Unlink-Spalte bereits vorhanden, überspringe'); return; } $table.data('unlink-added', true); - - debugLog('🔗 Füge Unlink-Spalte hinzu...'); + + debugLog('🔗 Füge Unlink-Spalte hinzu (Sections vorhanden)...'); // THEAD: Leere Spalte hinzufügen $table.find('thead tr').each(function() { @@ -1782,3 +1809,124 @@ function linkToNearestSection(lineId) { 'Abbrechen' ); } + +/** + * Importiert Sections und Textzeilen aus dem Ursprungsdokument + * (z.B. Angebot → Auftrag, Auftrag → Rechnung) + */ +function importFromOrigin() { + var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; + var docInfo = getDocumentInfo(); + + if (!docInfo.id) { + showErrorAlert(lang.importFromOriginError || 'Fehler: Keine Dokument-ID gefunden'); + return; + } + + debugLog('📥 Prüfe Import für ' + docInfo.type + ' #' + docInfo.id); + + // Zeige Ladehinweis + $('body').append('
| Produktgruppen: | ' + response.sections_count + ' |
| Textzeilen: | ' + response.textlines_count + ' |