391 lines
16 KiB
PHP
Executable file
391 lines
16 KiB
PHP
Executable file
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* Sync SubtotalTitle lines to/from facturedet
|
|
*/
|
|
|
|
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';
|
|
dol_include_once('/subtotaltitle/lib/subtotaltitle.lib.php');
|
|
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
|
|
|
|
header('Content-Type: application/json');
|
|
|
|
$action = GETPOST('action', 'alpha');
|
|
$line_id = GETPOST('line_id', 'int');
|
|
$line_type = GETPOST('line_type', 'alpha');
|
|
$facture_id = GETPOST('facture_id', 'int');
|
|
$docType = GETPOST('document_type', 'alpha');
|
|
|
|
subtotaltitle_debug_log('🔄 sync_to_facturedet: action='.$action.', line_id='.$line_id.', type='.$line_type.', docType='.$docType);
|
|
|
|
if (!$line_id || !$action || !$docType) {
|
|
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
|
|
exit;
|
|
}
|
|
|
|
// Hole die richtigen Tabellennamen für diesen Dokumenttyp
|
|
$tables = DocumentTypeHelper::getTableNames($docType);
|
|
if (!$tables) {
|
|
echo json_encode(array('success' => false, 'error' => 'Invalid document type'));
|
|
exit;
|
|
}
|
|
|
|
// Special codes für unsere Zeilentypen
|
|
$special_codes = array(
|
|
'section' => 100,
|
|
'text' => 101,
|
|
'subtotal' => 102
|
|
);
|
|
|
|
if ($action == 'add') {
|
|
// ========== ZUR RECHNUNG HINZUFÜGEN ==========
|
|
|
|
// Hole Daten aus unserer Manager-Tabelle
|
|
$sql = "SELECT m.*, s.title as section_title, s.rowid as section_rowid";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
|
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."facture_lines_manager s ON s.rowid = m.parent_section";
|
|
$sql .= " WHERE m.rowid = ".(int)$line_id;
|
|
$resql = $db->query($sql);
|
|
|
|
if (!$resql || $db->num_rows($resql) == 0) {
|
|
echo json_encode(array('success' => false, 'error' => 'Line not found'));
|
|
exit;
|
|
}
|
|
|
|
$line = $db->fetch_object($resql);
|
|
$document_id = $line->{$tables['fk_parent']};
|
|
$line_type = $line->line_type;
|
|
$fk_line_field = $tables['fk_line'];
|
|
|
|
// Prüfe ob schon in detail-Tabelle (für nicht-Produkte)
|
|
if ($line->$fk_line_field > 0 && $line_type != 'product') {
|
|
echo json_encode(array('success' => false, 'error' => 'Already in detail table'));
|
|
exit;
|
|
}
|
|
|
|
// AUTOMATISCHE REPARATUR: Wenn Section keine Subtotal-Zeile hat, erstelle sie
|
|
if ($line_type == 'section') {
|
|
$sql_check = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
|
$sql_check .= " WHERE parent_section = ".(int)$line_id;
|
|
$sql_check .= " AND line_type = 'subtotal'";
|
|
$sql_check .= " AND document_type = '".$db->escape($docType)."'";
|
|
$res_check = $db->query($sql_check);
|
|
|
|
if ($db->num_rows($res_check) == 0) {
|
|
// Keine Subtotal-Zeile vorhanden - automatisch erstellen
|
|
subtotaltitle_debug_log('⚠️ Section #'.$line_id.' hat keine Subtotal-Zeile - erstelle automatisch');
|
|
|
|
$sql_max = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
|
$sql_max .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
|
$sql_max .= " AND document_type = '".$db->escape($docType)."'";
|
|
$res_max = $db->query($sql_max);
|
|
$obj_max = $db->fetch_object($res_max);
|
|
$next_order = ($obj_max && $obj_max->max_order) ? $obj_max->max_order + 1 : 9999;
|
|
|
|
$fk_facture = ($docType === 'invoice') ? (int)$document_id : 'NULL';
|
|
$fk_propal = ($docType === 'propal') ? (int)$document_id : 'NULL';
|
|
$fk_commande = ($docType === 'order') ? (int)$document_id : 'NULL';
|
|
|
|
$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)$line_id.", ".$next_order.", NOW())";
|
|
$db->query($sql_subtotal);
|
|
|
|
subtotaltitle_debug_log('✅ Subtotal-Zeile automatisch erstellt für Section #'.$line_id);
|
|
}
|
|
}
|
|
|
|
// Bestimme special_code
|
|
$special_code = isset($special_codes[$line_type]) ? $special_codes[$line_type] : 0;
|
|
|
|
// Bestimme Beschreibung und Betrag
|
|
$description = '';
|
|
$total_ht = 0;
|
|
$qty = 0;
|
|
|
|
switch ($line_type) {
|
|
case 'section':
|
|
$description = $line->title;
|
|
$qty = 0;
|
|
break;
|
|
|
|
case 'text':
|
|
$description = $line->title;
|
|
$qty = 0;
|
|
break;
|
|
|
|
case 'subtotal':
|
|
// Berechne Summe der Section
|
|
$sql_sum = "SELECT SUM(d.total_ht) as total";
|
|
$sql_sum .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
|
|
$sql_sum .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
|
|
$sql_sum .= " WHERE m.parent_section = ".(int)$line->parent_section;
|
|
$sql_sum .= " AND m.document_type = '".$db->escape($docType)."'";
|
|
$sql_sum .= " AND m.line_type = 'product'";
|
|
$res_sum = $db->query($sql_sum);
|
|
$obj_sum = $db->fetch_object($res_sum);
|
|
$total_ht = $obj_sum->total ? $obj_sum->total : 0;
|
|
|
|
$description = 'Zwischensumme: '.$line->section_title;
|
|
$qty = 1;
|
|
break;
|
|
}
|
|
|
|
// Bestimme rang (Position) - UNTERSCHIEDLICH für Sections vs andere Zeilen
|
|
$new_rang = 1;
|
|
|
|
// EINHEITLICHE BERECHNUNG für ALLE Zeilentypen basierend auf line_order
|
|
// Finde den höchsten rang aller Zeilen, die VOR dieser Zeile (nach line_order) liegen
|
|
$sql_rang = "SELECT MAX(d.rang) as max_rang";
|
|
$sql_rang .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
|
|
$sql_rang .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
|
|
$sql_rang .= " WHERE m.".$tables['fk_parent']." = ".(int)$document_id;
|
|
$sql_rang .= " AND m.document_type = '".$db->escape($docType)."'";
|
|
$sql_rang .= " AND m.line_order < ".(int)$line->line_order;
|
|
$res_rang = $db->query($sql_rang);
|
|
$obj_rang = $db->fetch_object($res_rang);
|
|
$new_rang = ($obj_rang && $obj_rang->max_rang) ? $obj_rang->max_rang + 1 : 1;
|
|
|
|
subtotaltitle_debug_log('📝 Berechne rang: line_type='.$line_type.', new_rang='.$new_rang);
|
|
|
|
// Verschiebe alle nachfolgenden Zeilen
|
|
$sql_shift = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
|
|
$sql_shift .= " SET rang = rang + 1";
|
|
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
|
$sql_shift .= " AND rang >= ".(int)$new_rang;
|
|
$db->query($sql_shift);
|
|
|
|
// Füge neue Zeile in Detail-Tabelle ein
|
|
subtotaltitle_debug_log('📝 INSERT: line_type='.$line_type.', special_code='.$special_code);
|
|
|
|
$sql_ins = "INSERT INTO ".MAIN_DB_PREFIX.$tables['lines_table'];
|
|
$sql_ins .= " (".$tables['fk_parent'].", description, qty, subprice, total_ht, total_tva, total_ttc,";
|
|
$sql_ins .= " tva_tx, product_type, special_code, rang, info_bits)";
|
|
$sql_ins .= " VALUES (";
|
|
$sql_ins .= (int)$document_id.", ";
|
|
$sql_ins .= "'".$db->escape($description)."', ";
|
|
$sql_ins .= (float)$qty.", ";
|
|
$sql_ins .= ($line_type == 'subtotal') ? (float)$total_ht.", " : "0, ";
|
|
$sql_ins .= ($line_type == 'subtotal') ? (float)$total_ht.", " : "0, ";
|
|
$sql_ins .= "0, "; // total_tva
|
|
$sql_ins .= ($line_type == 'subtotal') ? (float)$total_ht.", " : "0, ";
|
|
$sql_ins .= "0, "; // tva_tx
|
|
$sql_ins .= "9, "; // product_type = 9 (Titel/Kommentar)
|
|
$sql_ins .= (int)$special_code.", ";
|
|
$sql_ins .= (int)$new_rang.", ";
|
|
$sql_ins .= "0)";
|
|
|
|
subtotaltitle_debug_log('📝 SQL: '.$sql_ins);
|
|
|
|
if (!$db->query($sql_ins)) {
|
|
echo json_encode(array('success' => false, 'error' => $db->lasterror()));
|
|
exit;
|
|
}
|
|
|
|
$new_detail_id = $db->last_insert_id(MAIN_DB_PREFIX.$tables['lines_table']);
|
|
|
|
// Update unsere Manager-Tabelle
|
|
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
|
|
$sql_upd .= " SET ".$tables['fk_line']." = ".(int)$new_detail_id;
|
|
$sql_upd .= ", in_facturedet = 1";
|
|
$sql_upd .= " WHERE rowid = ".(int)$line_id;
|
|
$db->query($sql_upd);
|
|
|
|
subtotaltitle_debug_log('✅ Zeile #'.$line_id.' zu '.$tables['lines_table'].' hinzugefügt als #'.$new_detail_id);
|
|
|
|
echo json_encode(array(
|
|
'success' => true,
|
|
'detail_id' => $new_detail_id,
|
|
'rang' => $new_rang
|
|
));
|
|
|
|
} elseif ($action == 'remove') {
|
|
// ========== AUS DETAIL-TABELLE ENTFERNEN ==========
|
|
|
|
// Hole Daten
|
|
$sql = "SELECT ".$tables['fk_line']." as detail_id, ".$tables['fk_parent']." as parent_id, line_type FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
|
$sql .= " WHERE rowid = ".(int)$line_id;
|
|
$resql = $db->query($sql);
|
|
|
|
if (!$resql || $db->num_rows($resql) == 0) {
|
|
echo json_encode(array('success' => false, 'error' => 'Line not found'));
|
|
exit;
|
|
}
|
|
|
|
$line = $db->fetch_object($resql);
|
|
|
|
// Produkte dürfen nicht entfernt werden
|
|
if ($line->line_type == 'product') {
|
|
echo json_encode(array('success' => false, 'error' => 'Cannot remove products'));
|
|
exit;
|
|
}
|
|
|
|
if (!$line->detail_id) {
|
|
echo json_encode(array('success' => false, 'error' => 'Not in detail table'));
|
|
exit;
|
|
}
|
|
|
|
// Hole rang bevor wir löschen
|
|
$sql_rang = "SELECT rang FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".(int)$line->detail_id;
|
|
$res_rang = $db->query($sql_rang);
|
|
$obj_rang = $db->fetch_object($res_rang);
|
|
$old_rang = $obj_rang ? $obj_rang->rang : 0;
|
|
|
|
// Lösche aus Detail-Tabelle
|
|
$sql_del = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
|
|
$sql_del .= " WHERE rowid = ".(int)$line->detail_id;
|
|
|
|
if (!$db->query($sql_del)) {
|
|
echo json_encode(array('success' => false, 'error' => $db->lasterror()));
|
|
exit;
|
|
}
|
|
|
|
// Schließe Lücke in rang
|
|
if ($old_rang > 0) {
|
|
$sql_shift = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
|
|
$sql_shift .= " SET rang = rang - 1";
|
|
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$line->parent_id;
|
|
$sql_shift .= " AND rang > ".(int)$old_rang;
|
|
$db->query($sql_shift);
|
|
}
|
|
|
|
// Update unsere Manager-Tabelle
|
|
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
|
|
$sql_upd .= " SET ".$tables['fk_line']." = NULL";
|
|
$sql_upd .= ", in_facturedet = 0";
|
|
$sql_upd .= " WHERE rowid = ".(int)$line_id;
|
|
$db->query($sql_upd);
|
|
|
|
subtotaltitle_debug_log('✅ Zeile #'.$line_id.' aus '.$tables['lines_table'].' entfernt');
|
|
|
|
echo json_encode(array('success' => true));
|
|
|
|
} elseif ($action == 'update_subtotal') {
|
|
// ========== SUBTOTAL-BETRAG AKTUALISIEREN ==========
|
|
|
|
$sql = "SELECT m.".$tables['fk_line']." as detail_id, m.parent_section, s.title as section_title";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
|
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."facture_lines_manager s ON s.rowid = m.parent_section";
|
|
$sql .= " WHERE m.rowid = ".(int)$line_id;
|
|
$sql .= " AND m.line_type = 'subtotal'";
|
|
$resql = $db->query($sql);
|
|
|
|
if (!$resql || $db->num_rows($resql) == 0) {
|
|
echo json_encode(array('success' => false, 'error' => 'Subtotal not found'));
|
|
exit;
|
|
}
|
|
|
|
$line = $db->fetch_object($resql);
|
|
|
|
if (!$line->detail_id) {
|
|
echo json_encode(array('success' => false, 'error' => 'Not in detail table'));
|
|
exit;
|
|
}
|
|
|
|
// Berechne neue Summe
|
|
$sql_sum = "SELECT SUM(d.total_ht) as total";
|
|
$sql_sum .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
|
|
$sql_sum .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
|
|
$sql_sum .= " WHERE m.parent_section = ".(int)$line->parent_section;
|
|
$sql_sum .= " AND m.document_type = '".$db->escape($docType)."'";
|
|
$sql_sum .= " AND m.line_type = 'product'";
|
|
$res_sum = $db->query($sql_sum);
|
|
$obj_sum = $db->fetch_object($res_sum);
|
|
$total_ht = $obj_sum->total ? $obj_sum->total : 0;
|
|
|
|
// Update Detail-Tabelle
|
|
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
|
|
$sql_upd .= " SET subprice = ".(float)$total_ht;
|
|
$sql_upd .= ", total_ht = ".(float)$total_ht;
|
|
$sql_upd .= ", total_ttc = ".(float)$total_ht;
|
|
$sql_upd .= " WHERE rowid = ".(int)$line->detail_id;
|
|
$db->query($sql_upd);
|
|
|
|
subtotaltitle_debug_log('✅ Subtotal #'.$line_id.' aktualisiert: '.$total_ht);
|
|
|
|
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'));
|
|
}
|