subtotaltitle/ajax/import_from_origin.php

565 lines
25 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* 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'));
}