* * 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')); }