subtotaltitle/class/actions_subtotaltitle.class.php

1728 lines
80 KiB
PHP
Executable file

<?php
/* Copyright (C) 2023 Laurent Destailleur <eldy@users.sourceforge.net>
* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* \file subtotaltitle/class/actions_subtotaltitle.class.php
* \ingroup subtotaltitle
* \brief Hook overload for SubtotalTitle module
*/
require_once DOL_DOCUMENT_ROOT.'/core/class/commonhookactions.class.php';
require_once __DIR__.'/DocumentTypeHelper.class.php';
/**
* Class ActionsSubtotalTitle
*/
class ActionsSubtotalTitle extends CommonHookActions
{
private $debug = 0; // Wird dynamisch aus Config geladen
// Gemeinsames Array für gerenderte Sections
private static $rendered_sections = array();
// Aktueller Dokumentkontext (wird in formObjectOptions/printObjectLine gesetzt)
private $currentDocType = null;
private $currentDocumentId = null;
private $isDraft = false;
/**
* @var DoliDB Database handler.
*/
public $db;
/**
* Helper-Methode: Erstellt WHERE-Klausel für document_id
*/
private function getDocumentWhere($document_id, $docType, $tableAlias = 'm')
{
global $db;
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) return "";
$prefix = $tableAlias ? $tableAlias."." : "";
return " WHERE ".$prefix.$tables['fk_parent']." = ".(int)$document_id." AND ".$prefix."document_type = '".$db->escape($docType)."'";
}
/**
* @var string Error code (or message)
*/
public $error = '';
/**
* @var string[] Errors
*/
public $errors = array();
/**
* @var mixed[] Hook results. Propagated to $hookmanager->resArray for later reuse
*/
public $results = array();
/**
* @var ?string String displayed by executeHook() immediately after return
*/
public $resprints;
/**
* @var int Priority of hook (50 is used if value is not defined)
*/
public $priority;
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
global $conf;
$this->db = $db;
// Debug-Modus aus Config laden
$this->debug = getDolGlobalInt('SUBTOTALTITLE_DEBUG_MODE', 0);
// IMMER Debug-Log in Datei schreiben
$logFile = '/srv/http/dolibarr/documents/subtotaltitle/debug.log';
@mkdir(dirname($logFile), 0777, true);
error_log('['.date('Y-m-d H:i:s').'] SubtotalTitle Constructor aufgerufen'."\n", 3, $logFile);
}
public function formObjectOptions($parameters, &$object, &$action, $hookmanager)
{
global $conf, $langs;
// Prüfe ob es ein unterstützter Kontext ist
$supportedContexts = array('invoicecard', 'propalcard', 'ordercard');
$currentContexts = explode(':', $parameters['currentcontext']);
$isSupported = false;
foreach ($supportedContexts as $ctx) {
if (in_array($ctx, $currentContexts)) {
$isSupported = true;
break;
}
}
if ($isSupported) {
// Setze Klassenvariablen für den aktuellen Dokumentkontext
$this->currentDocType = DocumentTypeHelper::getTypeFromObject($object);
$this->currentDocumentId = $object->id;
// Prüfe ob Dokument bearbeitbar ist (nur im Entwurfsstatus)
$this->isDraft = false;
if (isset($object->statut)) {
$this->isDraft = ($object->statut == 0);
// Debug-Log
$logFile = '/srv/http/dolibarr/documents/subtotaltitle/debug.log';
error_log('['.date('Y-m-d H:i:s').'] formObjectOptions - DocType: '.$this->currentDocType.', statut: '.$object->statut.', isDraft: '.($this->isDraft ? 'true' : 'false')."\n", 3, $logFile);
} elseif (isset($object->status)) {
$this->isDraft = ($object->status == 0);
// Debug-Log
$logFile = '/srv/http/dolibarr/documents/subtotaltitle/debug.log';
error_log('['.date('Y-m-d H:i:s').'] formObjectOptions - DocType: '.$this->currentDocType.', status: '.$object->status.', isDraft: '.($this->isDraft ? 'true' : 'false')."\n", 3, $logFile);
}
$is_draft = $this->isDraft;
// Lade Übersetzungen
$langs->load('subtotaltitle@subtotaltitle');
// Prüfe ob Sections oder Textzeilen existieren (für Buttons)
global $db;
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
$hasSections = false;
$hasTextLines = false;
$hasSectionsOrTextLines = false;
if ($tables && $object->id) {
// Prüfe Sections
$sql_sec = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_sec .= " WHERE ".$tables['fk_parent']." = ".(int)$object->id;
$sql_sec .= " AND document_type = '".$db->escape($this->currentDocType)."'";
$sql_sec .= " AND line_type = 'section'";
$res_sec = $db->query($sql_sec);
if ($res_sec && $obj_sec = $db->fetch_object($res_sec)) {
$hasSections = ($obj_sec->cnt > 0);
}
// Prüfe Textzeilen
$sql_text = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_text .= " WHERE ".$tables['fk_parent']." = ".(int)$object->id;
$sql_text .= " AND document_type = '".$db->escape($this->currentDocType)."'";
$sql_text .= " AND line_type = 'text'";
$res_text = $db->query($sql_text);
if ($res_text && $obj_text = $db->fetch_object($res_text)) {
$hasTextLines = ($obj_text->cnt > 0);
}
$hasSectionsOrTextLines = ($hasSections || $hasTextLines);
}
// CSS
$cssPath = dol_buildpath('/custom/subtotaltitle/css/subtotaltitle.css', 1);
echo '<link rel="stylesheet" type="text/css" href="'.$cssPath.'">'."\n";
// Übersetzungen als JavaScript-Variablen bereitstellen
echo '<script type="text/javascript">'."\n";
echo 'var subtotalTitleIsDraft = '.($this->isDraft ? 'true' : 'false').';'."\n";
echo 'var subtotalTitleHasSections = '.($hasSections ? 'true' : 'false').';'."\n";
// AJAX-URL-Pfad für alle AJAX-Aufrufe
echo 'var subtotaltitleAjaxUrl = "'.dol_buildpath('/subtotaltitle/ajax/', 1).'";'."\n";
// Grip-Bild-Pfad für Drag&Drop (wie Dolibarr es macht)
echo 'var subtotalTitleGripUrl = "'.DOL_URL_ROOT.'/theme/'.$conf->theme.'/img/grip.png";'."\n";
echo 'var subtotalTitleLang = {'."\n";
echo ' buttonCreateTextline: '.json_encode($langs->trans('ButtonCreateTextline')).','."\n";
echo ' buttonToInvoice: '.json_encode($langs->trans('ButtonToInvoice')).','."\n";
echo ' buttonFromInvoice: '.json_encode($langs->trans('ButtonFromInvoice')).','."\n";
echo ' buttonExpandAll: '.json_encode($langs->trans('ButtonExpandAll')).','."\n";
echo ' buttonCollapseAll: '.json_encode($langs->trans('ButtonCollapseAll')).','."\n";
echo ' buttonMassDelete: '.json_encode($langs->trans('ButtonMassDelete')).','."\n";
echo ' buttonDelete: '.json_encode($langs->trans('ButtonDelete')).','."\n";
echo ' buttonEdit: '.json_encode($langs->trans('ButtonEdit')).','."\n";
echo ' buttonSave: '.json_encode($langs->trans('ButtonSave')).','."\n";
echo ' buttonCancel: '.json_encode($langs->trans('ButtonCancel')).','."\n";
echo ' sectionCreate: '.json_encode($langs->trans('SectionCreate')).','."\n";
echo ' sectionName: '.json_encode($langs->trans('SectionName')).','."\n";
echo ' sectionSubtotal: '.json_encode($langs->trans('SectionSubtotal')).','."\n";
echo ' productCount: '.json_encode($langs->trans('ProductCount')).','."\n";
echo ' confirmMassDelete: '.json_encode($langs->trans('ConfirmMassDelete')).','."\n";
echo ' confirmDeleteSection: '.json_encode($langs->trans('ConfirmDeleteSection')).','."\n";
echo ' confirmDeleteSectionForce: '.json_encode($langs->trans('ConfirmDeleteSectionForce')).','."\n";
echo ' confirmDeleteSectionForce2: '.json_encode($langs->trans('ConfirmDeleteSectionForce2')).','."\n";
echo ' confirmDeleteTextline: '.json_encode($langs->trans('ConfirmDeleteTextline')).','."\n";
echo ' confirmRemoveFromSection: '.json_encode($langs->trans('ConfirmRemoveFromSection')).','."\n";
echo ' confirmDeleteLines: '.json_encode($langs->trans('ConfirmDeleteLines')).','."\n";
echo ' confirmDeleteLinesWarning: '.json_encode($langs->trans('ConfirmDeleteLinesWarning')).','."\n";
echo ' noLinesSelected: '.json_encode($langs->trans('NoLinesSelected')).','."\n";
echo ' errorLoadingSections: '.json_encode($langs->trans('ErrorLoadingSections')).','."\n";
echo ' errorSavingSection: '.json_encode($langs->trans('ErrorSavingSection')).','."\n";
echo ' errorDeletingSection: '.json_encode($langs->trans('ErrorDeletingSection')).','."\n";
echo ' errorReordering: '.json_encode($langs->trans('ErrorReordering')).','."\n";
echo ' successSectionCreated: '.json_encode($langs->trans('SuccessSectionCreated')).','."\n";
echo ' successSectionUpdated: '.json_encode($langs->trans('SuccessSectionUpdated')).','."\n";
echo ' successSectionDeleted: '.json_encode($langs->trans('SuccessSectionDeleted')).','."\n";
echo ' successReordered: '.json_encode($langs->trans('SuccessReordered')).','."\n";
echo ' textlineContent: '.json_encode($langs->trans('TextlineContent')).','."\n";
echo ' errorSavingTextline: '.json_encode($langs->trans('ErrorSavingTextline')).','."\n";
echo ' errorDeletingTextline: '.json_encode($langs->trans('ErrorDeletingTextline')).','."\n";
echo ' successTextlineCreated: '.json_encode($langs->trans('SuccessTextlineCreated')).','."\n";
echo ' successTextlineUpdated: '.json_encode($langs->trans('SuccessTextlineUpdated')).','."\n";
echo ' successTextlineDeleted: '.json_encode($langs->trans('SuccessTextlineDeleted')).','."\n";
echo ' syncToInvoice: '.json_encode($langs->trans('SyncToInvoice')).','."\n";
echo ' syncFromInvoice: '.json_encode($langs->trans('SyncFromInvoice')).','."\n";
echo ' confirmRemoveLine: '.json_encode($langs->trans('ConfirmRemoveLine')).','."\n";
echo ' confirmSyncAll: '.json_encode($langs->trans('ConfirmSyncAll')).','."\n";
echo ' confirmRemoveAll: '.json_encode($langs->trans('ConfirmRemoveAll')).','."\n";
echo ' allElementsAlreadyInInvoice: '.json_encode($langs->trans('AllElementsAlreadyInInvoice')).','."\n";
echo ' noElementsInInvoice: '.json_encode($langs->trans('NoElementsInInvoice')).','."\n";
echo ' elementsAddedToInvoice: '.json_encode($langs->trans('ElementsAddedToInvoice')).','."\n";
echo ' elementsRemovedFromInvoice: '.json_encode($langs->trans('ElementsRemovedFromInvoice')).','."\n";
echo ' elementsAddedWithErrors: '.json_encode($langs->trans('ElementsAddedWithErrors')).','."\n";
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";
// 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 '</script>'."\n";
// Haupt-JavaScript
$jsPath = dol_buildpath('/custom/subtotaltitle/js/subtotaltitle.js', 1);
echo '<script type="text/javascript" src="'.$jsPath.'"></script>'."\n";
// Sync-JavaScript (NEU!)
$jsSyncPath = dol_buildpath('/custom/subtotaltitle/js/subtotaltitle_sync.js', 1);
echo '<script type="text/javascript" src="'.$jsSyncPath.'"></script>'."\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
echo '<script>$(document).ready(function() {';
echo ' if ($(".tabsAction").length > 0) {';
echo ' $(".tabsAction").find("a.butAction:first").after(\'<a class="butAction" href="#" onclick="createTextLine(); return false;">\' + subtotalTitleLang.buttonCreateTextline + \'</a>\');';
echo ' }';
echo '});</script>'."\n";
// Import-Button (wenn Ursprungsdokument existiert)
// Button erscheint immer - die check-Action prüft ob es etwas zu importieren gibt
if ($hasOrigin) {
echo '<script>$(document).ready(function() {';
echo ' if ($(".tabsAction").length > 0 && $("#btnImportOrigin").length === 0) {';
echo ' $(".tabsAction").first().append(\'<a id="btnImportOrigin" class="butAction" href="#" onclick="importFromOrigin(); return false;" title="Produktgruppen aus Ursprungsdokument übernehmen">📥 Import</a>\');';
echo ' }';
echo '});</script>'."\n";
}
// Massenlösch-Button (ans ENDE der Hauptzeile) - NUR EINMAL EINFÜGEN
echo '<script>$(document).ready(function() {';
echo ' if ($("#btnMassDelete").length === 0) {';
echo ' $(".tabsAction").first().append(\'<a id="btnMassDelete" class="butAction" href="#" onclick="toggleMassDelete(); return false;" style="margin-left:20px;">Zeilen löschen</a>\');';
echo ' }';
echo '});</script>'."\n";
// Sync-Buttons + Collapse-Buttons - rechts ausgerichtet
// Nur anzeigen wenn Sections oder Textzeilen vorhanden sind
if ($hasSectionsOrTextLines) {
echo '<script>$(document).ready(function() {';
echo ' if ($(".sync-collapse-row").length === 0) {';
echo ' var buttons = \'<div class="sync-collapse-row" style="text-align:right; margin:5px 0;">\';';
echo ' buttons += \'<a class="button" href="#" onclick="syncAllToFacturedet(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonToInvoice + \'</a>\';';
echo ' buttons += \'<a class="button" href="#" onclick="removeAllFromFacturedet(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonFromInvoice + \'</a>\';';
// Collapse-Buttons nur wenn Sections existieren (aus PHP)
if ($hasSections) {
echo ' buttons += \'<a class="button" href="#" onclick="expandAllSections(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonExpandAll + \'</a>\';';
echo ' buttons += \'<a class="button" href="#" onclick="collapseAllSections(); return false;" style="margin-left:5px;">\' + subtotalTitleLang.buttonCollapseAll + \'</a>\';';
}
echo ' buttons += \'</div>\';';
echo ' $(".tabsAction").first().after(buttons);';
echo ' }';
echo '});</script>'."\n";
}
}
}
return 0;
}
/**
* Execute action
*/
public function getNomUrl($parameters, &$object, &$action)
{
global $db, $langs, $conf, $user;
$this->resprints = '';
return 0;
}
/**
* Overload the doActions function
* Reagiert auf Lösch-Aktionen um die Manager-Tabelle zu aktualisieren
*/
public function doActions($parameters, &$object, &$action, $hookmanager)
{
global $db, $conf;
// Reagiere auf Zeilen-Löschung
if ($action == 'confirm_deleteline' && !empty($object->id)) {
$lineid = GETPOST('lineid', 'int');
if ($lineid > 0) {
// Bestimme Dokumenttyp
$docType = 'invoice';
if (get_class($object) == 'Propal') {
$docType = 'propal';
} elseif (get_class($object) == 'Commande') {
$docType = 'order';
}
require_once __DIR__.'/DocumentTypeHelper.class.php';
$tables = DocumentTypeHelper::getTableNames($docType);
if ($tables) {
// Lösche aus Manager-Tabelle
$sql = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_line']." = ".(int)$lineid;
$db->query($sql);
// Nummeriere line_order neu durch
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$object->id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$new_order = 1;
while ($obj = $db->fetch_object($resql)) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager SET line_order = ".$new_order." WHERE rowid = ".(int)$obj->rowid;
$db->query($sql_upd);
$new_order++;
}
// Synchronisiere rang in Detail-Tabelle
$sql = "SELECT rowid, ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$object->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)) {
if ($obj->detail_id) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table']." SET rang = ".$rang." WHERE rowid = ".(int)$obj->detail_id;
$db->query($sql_upd);
$rang++;
}
}
}
}
}
return 0;
}
/**
* Overload the doMassActions function
*/
public function doMassActions($parameters, &$object, &$action, $hookmanager)
{
global $conf, $user, $langs;
$error = 0;
if (in_array($parameters['currentcontext'], array('somecontext1', 'somecontext2'))) {
foreach ($parameters['toselect'] as $objectid) {
// Do action on each object id
}
if (!$error) {
$this->results = array('myreturn' => 999);
$this->resprints = 'A text to show';
return 0;
} else {
$this->errors[] = 'Error message';
return -1;
}
}
return 0;
}
/**
* Overload the addMoreMassActions function
*/
public function addMoreMassActions($parameters, &$object, &$action, $hookmanager)
{
global $conf, $user, $langs;
$error = 0;
$disabled = 1;
if (in_array($parameters['currentcontext'], array('somecontext1', 'somecontext2'))) {
$this->resprints = '<option value="0"'.($disabled ? ' disabled="disabled"' : '').'>'.$langs->trans("SubtotalTitleMassAction").'</option>';
}
if (!$error) {
return 0;
} else {
$this->errors[] = 'Error message';
return -1;
}
}
/**
* Execute action before PDF (document) creation
*/
public function beforePDFCreation($parameters, &$object, &$action)
{
global $conf, $user, $langs;
global $hookmanager;
$outputlangs = $langs;
$ret = 0;
$deltemp = array();
dol_syslog(get_class($this).'::executeHooks action='.$action);
if (in_array($parameters['currentcontext'], array('somecontext1', 'somecontext2'))) {
// do something only for the context 'somecontext1' or 'somecontext2'
}
return $ret;
}
/**
* Execute action after PDF (document) creation
*/
public function afterPDFCreation($parameters, &$pdfhandler, &$action)
{
global $conf, $user, $langs;
global $hookmanager;
$outputlangs = $langs;
$ret = 0;
$deltemp = array();
dol_syslog(get_class($this).'::executeHooks action='.$action);
if (in_array($parameters['currentcontext'], array('somecontext1', 'somecontext2'))) {
// do something only for the context 'somecontext1' or 'somecontext2'
}
return $ret;
}
/**
* Overload the loadDataForCustomReports function
*/
public function loadDataForCustomReports($parameters, &$action, $hookmanager)
{
global $langs;
$langs->load("subtotaltitle@subtotaltitle");
$this->results = array();
$head = array();
$h = 0;
if ($parameters['tabfamily'] == 'subtotaltitle') {
$head[$h][0] = dol_buildpath('/module/index.php', 1);
$head[$h][1] = $langs->trans("Home");
$head[$h][2] = 'home';
$h++;
$this->results['title'] = $langs->trans("SubtotalTitle");
$this->results['picto'] = 'subtotaltitle@subtotaltitle';
}
$head[$h][0] = 'customreports.php?objecttype='.$parameters['objecttype'].(empty($parameters['tabfamily']) ? '' : '&tabfamily='.$parameters['tabfamily']);
$head[$h][1] = $langs->trans("CustomReports");
$head[$h][2] = 'customreports';
$this->results['head'] = $head;
$arrayoftypes = array();
$this->results['arrayoftype'] = $arrayoftypes;
return 0;
}
/**
* Overload the restrictedArea function
*/
public function restrictedArea($parameters, $object, &$action, $hookmanager)
{
global $user;
if ($parameters['features'] == 'myobject') {
if ($user->hasRight('subtotaltitle', 'myobject', 'read')) {
$this->results['result'] = 1;
return 1;
} else {
$this->results['result'] = 0;
return 1;
}
}
return 0;
}
/**
* Execute action completeTabsHead
*/
public function completeTabsHead(&$parameters, &$object, &$action, $hookmanager)
{
global $langs, $conf, $user;
if (!isset($parameters['object']->element)) {
return 0;
}
if ($parameters['mode'] == 'remove') {
return 0;
} elseif ($parameters['mode'] == 'add') {
$langs->load('subtotaltitle@subtotaltitle');
$counter = count($parameters['head']);
$element = $parameters['object']->element;
$id = $parameters['object']->id;
if (in_array($element, ['context1', 'context2'])) {
$datacount = 0;
$parameters['head'][$counter][0] = dol_buildpath('/subtotaltitle/subtotaltitle_tab.php', 1) . '?id=' . $id . '&amp;module='.$element;
$parameters['head'][$counter][1] = $langs->trans('SubtotalTitleTab');
if ($datacount > 0) {
$parameters['head'][$counter][1] .= '<span class="badge marginleftonlyshort">' . $datacount . '</span>';
}
$parameters['head'][$counter][2] = 'subtotaltitleemails';
$counter++;
}
if ($counter > 0 && (int) DOL_VERSION < 14) {
$this->results = $parameters['head'];
return 1;
} else {
return 0;
}
} else {
return -1;
}
}
/**
* Overload the showLinkToObjectBlock function
*/
public function showLinkToObjectBlock($parameters, &$object, &$action, $hookmanager)
{
return 0;
}
/**
* Hook: Wird aufgerufen wenn eine Rechnung geladen wird
* Synchronisiert die Tabelle und rendert Section-Header
*/
public function printObjectLine($parameters, &$object, &$action, $hookmanager)
{
global $db, $langs;
// Erkenne Dokumenttyp
$docType = DocumentTypeHelper::getTypeFromObject($object);
if (!$docType) {
return 0;
}
// Prüfe Context
$expectedContext = DocumentTypeHelper::getContext($docType);
if ($parameters['currentcontext'] != $expectedContext) {
return 0;
}
$document_id = $object->id;
// Setze Klassenvariablen für den aktuellen Dokumentkontext
$this->currentDocType = $docType;
$this->currentDocumentId = $document_id;
// Prüfe ob Dokument bearbeitbar ist (nur im Entwurfsstatus)
$this->isDraft = false;
if (isset($object->statut)) {
$this->isDraft = ($object->statut == 0);
// Debug-Log
$logFile = '/srv/http/dolibarr/documents/subtotaltitle/debug.log';
error_log('['.date('Y-m-d H:i:s').'] printObjectLine - DocType: '.$this->currentDocType.', statut: '.$object->statut.', isDraft: '.($this->isDraft ? 'true' : 'false')."\n", 3, $logFile);
} elseif (isset($object->status)) {
$this->isDraft = ($object->status == 0);
// Debug-Log
$logFile = '/srv/http/dolibarr/documents/subtotaltitle/debug.log';
error_log('['.date('Y-m-d H:i:s').'] printObjectLine - DocType: '.$this->currentDocType.', status: '.$object->status.', isDraft: '.($this->isDraft ? 'true' : 'false')."\n", 3, $logFile);
}
$line = $parameters['line'];
if (!$line || !$line->id) {
return 0;
}
// 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 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;
}
// Dokument neu laden um aktuelle rang zu haben
static $reloaded = array();
$reload_key = $docType.'_'.$document_id;
if (!isset($reloaded[$reload_key])) {
$object->fetch($document_id);
$object->fetch_thirdparty();
$object->fetch_lines();
$reloaded[$reload_key] = true;
}
// Synchronisiere Manager-Tabelle
$this->syncManagerTable($document_id, $docType);
// Rendere alle Sections die VOR dieser Zeile kommen sollten (inkl. leere!)
$this->renderAllPendingSections($document_id, $line, $docType);
// WICHTIG: Markiere diese Zeile mit line_order UND parent_section für JavaScript
$tables = DocumentTypeHelper::getTableNames($docType);
$sql = "SELECT line_order, parent_section FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_line']." = ".(int)$line->id;
$resql = $db->query($sql);
if ($obj = $db->fetch_object($resql)) {
$parentSection = $obj->parent_section ? $obj->parent_section : 'null';
echo '<script>$(document).ready(function() { ';
echo ' $("tr[id*=\''.$line->id.'\']").attr("data-line-order", '.$obj->line_order.');';
echo ' $("tr[id*=\''.$line->id.'\']").attr("data-parent-section", "'.$parentSection.'");';
echo ' $("tr[id*=\''.$line->id.'\']").attr("data-document-type", "'.$docType.'");';
echo '});</script>';
}
return 0;
}
/**
* Rendert ALLE Sections die VOR dieser RANG-Position kommen sollten
*/
private function renderAllPendingSections($document_id, $line, $docType)
{
global $db;
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) return;
static $last_rang = array();
static $last_parent_section = array();
static $last_line_order = array();
$doc_key = $docType.'_'.$document_id;
if (!isset(self::$rendered_sections[$doc_key])) {
self::$rendered_sections[$doc_key] = array();
$last_rang[$doc_key] = 0;
$last_parent_section[$doc_key] = null;
$last_line_order[$doc_key] = 0;
}
// Hole rang dieser Produktzeile
$sql = "SELECT rang FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql .= " WHERE rowid = ".(int)$line->id;
$resql = $db->query($sql);
if (!$resql || $db->num_rows($resql) == 0) return;
$current_rang = $db->fetch_object($resql)->rang;
// Hole line_order und parent_section des aktuellen Produkts aus Manager-Tabelle
$sql_current = "SELECT line_order, parent_section FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_current .= " WHERE ".$tables['fk_line']." = ".(int)$line->id." AND document_type = '".$db->escape($docType)."'";
$res_current = $db->query($sql_current);
$current_line_order = 0;
$current_parent_section = null;
if ($obj_current = $db->fetch_object($res_current)) {
$current_line_order = $obj_current->line_order;
$current_parent_section = $obj_current->parent_section;
}
// 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) {
// 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 ($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 ($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)');
}
}
}
}
}
// Hole ALLE Sections und Textzeilen die VOR dieser Produktzeile kommen
// Kombiniert nach line_order sortiert, damit Textzeilen VOR Sections erscheinen können
$sql_combined = "SELECT rowid, title, line_type, line_order, show_subtotal, collapsed, in_facturedet,";
$sql_combined .= " (SELECT MIN(fd.rang) FROM ".MAIN_DB_PREFIX."facture_lines_manager m2";
$sql_combined .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." fd ON fd.rowid = m2.".$tables['fk_line'];
$sql_combined .= " WHERE m2.parent_section = ".MAIN_DB_PREFIX."facture_lines_manager.rowid";
$sql_combined .= " AND m2.document_type = '".$db->escape($docType)."'";
$sql_combined .= " AND m2.line_type = 'product') as first_product_rang"; // NUR echte Produkte, keine Subtotals!
$sql_combined .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_combined .= $this->getDocumentWhere($document_id, $docType, '');
$sql_combined .= " AND (line_type = 'section' OR line_type = 'text')";
$sql_combined .= " AND line_order < ".(int)$current_line_order;
$sql_combined .= " ORDER BY line_order";
$resql_combined = $db->query($sql_combined);
while ($obj = $db->fetch_object($resql_combined)) {
if ($obj->line_type == 'section') {
// Section rendern wenn:
// 1. Sie Produkte hat UND first_product_rang im Bereich liegt, ODER
// 2. Sie KEINE Produkte hat (leere Section) UND ihre line_order zwischen last_line_order und current_line_order liegt
$should_render = false;
// DEBUG
error_log('[SubtotalTitle] DEBUG Section "'.$obj->title.'": line_order='.$obj->line_order.', first_product_rang='.($obj->first_product_rang ?? 'NULL').', last_line_order='.$last_line_order[$doc_key].', last_rang='.$last_rang[$doc_key].', current_rang='.$current_rang);
if ($obj->first_product_rang !== null) {
// Section mit Produkten: prüfe rang-Bereich
if ($obj->first_product_rang > (int)$last_rang[$doc_key] && $obj->first_product_rang <= (int)$current_rang) {
$should_render = true;
}
} else {
// Leere Section: rendern wenn line_order > last_line_order UND < current_line_order
// (SQL filtert bereits nach line_order < current_line_order)
if ($obj->line_order > (int)$last_line_order[$doc_key]) {
$should_render = true;
error_log('[SubtotalTitle] DEBUG → Leere Section "'.$obj->title.'" SOLLTE gerendert werden');
}
}
if ($should_render && !in_array($obj->rowid, self::$rendered_sections[$doc_key])) {
$section = array(
'section_id' => $obj->rowid,
'title' => $obj->title,
'show_subtotal' => $obj->show_subtotal,
'collapsed' => $obj->collapsed,
'in_facturedet' => $obj->in_facturedet
);
echo $this->renderSectionHeader($section);
self::$rendered_sections[$doc_key][] = $obj->rowid;
if ($this->debug) {
error_log('[SubtotalTitle] ✅ Section "'.$obj->title.'" gerendert vor rang '.$current_rang.' (leere Section: '.($obj->first_product_rang === null ? 'ja' : 'nein').')');
}
// Für LEERE Sections: Subtotal direkt nach Section-Header rendern (wenn show_subtotal aktiviert)
if ($obj->first_product_rang === null && $obj->show_subtotal) {
$sql_subtotal_empty = "SELECT rowid, title, parent_section, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_subtotal_empty .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_subtotal_empty .= " AND document_type = '".$db->escape($docType)."'";
$sql_subtotal_empty .= " AND parent_section = ".(int)$obj->rowid;
$sql_subtotal_empty .= " AND line_type = 'subtotal'";
$resql_subtotal_empty = $db->query($sql_subtotal_empty);
if ($obj_sub_empty = $db->fetch_object($resql_subtotal_empty)) {
$subtotal_key_empty = 'subtotal_'.$obj_sub_empty->rowid;
if (!in_array($subtotal_key_empty, self::$rendered_sections[$doc_key])) {
echo $this->renderSubtotalLine($obj_sub_empty);
self::$rendered_sections[$doc_key][] = $subtotal_key_empty;
if ($this->debug) {
error_log('[SubtotalTitle] ✅ Subtotal für leere Section "'.$obj->title.'" gerendert');
}
}
}
}
}
} elseif ($obj->line_type == 'text') {
// VOR der Textzeile: Prüfe ob es leere Sections gibt, die noch nicht gerendert wurden
// und deren line_order kleiner ist als die der Textzeile
$sql_empty_sections = "SELECT rowid, title, line_order, show_subtotal, collapsed, in_facturedet,";
$sql_empty_sections .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."facture_lines_manager m2";
$sql_empty_sections .= " WHERE m2.parent_section = ".MAIN_DB_PREFIX."facture_lines_manager.rowid";
$sql_empty_sections .= " AND m2.document_type = '".$db->escape($docType)."'";
$sql_empty_sections .= " AND m2.line_type = 'product') as product_count";
$sql_empty_sections .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_empty_sections .= $this->getDocumentWhere($document_id, $docType, '');
$sql_empty_sections .= " AND line_type = 'section'";
$sql_empty_sections .= " AND line_order < ".(int)$obj->line_order;
$sql_empty_sections .= " ORDER BY line_order";
$resql_empty = $db->query($sql_empty_sections);
while ($empty_sec = $db->fetch_object($resql_empty)) {
// Nur leere Sections (keine Produkte) die noch nicht gerendert wurden
if ($empty_sec->product_count == 0 && !in_array($empty_sec->rowid, self::$rendered_sections[$doc_key])) {
$section_data = array(
'section_id' => $empty_sec->rowid,
'title' => $empty_sec->title,
'show_subtotal' => $empty_sec->show_subtotal,
'collapsed' => $empty_sec->collapsed,
'in_facturedet' => $empty_sec->in_facturedet
);
echo $this->renderSectionHeader($section_data);
self::$rendered_sections[$doc_key][] = $empty_sec->rowid;
if ($this->debug) {
error_log('[SubtotalTitle] ✅ Leere Section "'.$empty_sec->title.'" vor Textzeile gerendert');
}
// Subtotal für diese Section (wenn aktiviert)
if ($empty_sec->show_subtotal) {
$sql_sub = "SELECT rowid, title, parent_section, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_sub .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_sub .= " AND document_type = '".$db->escape($docType)."'";
$sql_sub .= " AND parent_section = ".(int)$empty_sec->rowid;
$sql_sub .= " AND line_type = 'subtotal'";
$resql_sub = $db->query($sql_sub);
if ($obj_sub = $db->fetch_object($resql_sub)) {
echo $this->renderSubtotalLine($obj_sub);
self::$rendered_sections[$doc_key][] = 'subtotal_'.$obj_sub->rowid;
}
}
}
}
// Textzeile rendern
$text_key = 'text_'.$obj->rowid;
if (!in_array($text_key, self::$rendered_sections[$doc_key])) {
$textline = array(
'id' => $obj->rowid,
'title' => $obj->title,
'line_order' => $obj->line_order,
'in_facturedet' => $obj->in_facturedet
);
echo $this->renderTextLine($textline);
self::$rendered_sections[$doc_key][] = $text_key;
if ($this->debug) {
error_log('[SubtotalTitle] ✅ Textzeile "'.$obj->title.'" gerendert');
}
}
}
}
// Merke für nächsten Durchlauf
$last_rang[$doc_key] = $current_rang;
$last_parent_section[$doc_key] = $current_parent_section;
$last_line_order[$doc_key] = $current_line_order;
// 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');
}
}
}
}
// Trailing items (leere Sections, Textzeilen) werden jetzt in formAddObjectLine gerendert
// da dieser Hook NACH allen Produktzeilen aber VOR dem Eingabeformular aufgerufen wird
}
}
}
/**
* Rendert eine Textzeile
*/
private function renderTextLine($textline)
{
global $db;
// In-Facturedet Status
$in_facturedet = isset($textline['in_facturedet']) ? $textline['in_facturedet'] : 0;
$sync_checked = $in_facturedet ? 'checked' : '';
$in_class = $in_facturedet ? ' in-facturedet' : '';
$html = '<tr class="textline-row drag'.$in_class.'" data-textline-id="'.$textline['id'].'" data-line-order="'.$textline['line_order'].'">';
// Titel (colspan=6) - wie bei Sections
$html .= '<td colspan="6" style="padding:8px; font-weight:bold;">';
$html .= htmlspecialchars($textline['title']);
$html .= '</td>';
// Sync-Checkbox (colspan=2) - wie bei Sections
$html .= '<td colspan="2" align="right">';
if ($this->isDraft) {
$html .= '<label style="font-weight:normal;font-size:12px;" title="In Rechnung/PDF anzeigen">';
$html .= '<input type="checkbox" class="sync-checkbox" data-line-id="'.$textline['id'].'" data-line-type="text" '.$sync_checked.' onclick="toggleFacturedetSync('.$textline['id'].', \'text\', this);">';
$html .= ' 📄</label>';
}
$html .= '</td>';
// Leer (colspan=2) - Platzhalter wie bei Sections für Move-Buttons
$html .= '<td colspan="2"></td>';
// Edit (Spalte 11) - nur im Entwurfsstatus
$html .= '<td class="linecoledit center">';
if ($this->isDraft) {
$html .= '<a href="#" onclick="editTextLine('.$textline['id'].', \''.addslashes($textline['title']).'\'); return false;" title="Bearbeiten">';
$html .= '<span class="fas fa-pencil-alt" style="color:#444;" title="Ändern"></span></a>';
}
$html .= '</td>';
// Delete (Spalte 12) - nur im Entwurfsstatus
$html .= '<td class="linecoldelete center">';
if ($this->isDraft) {
$html .= '<a href="#" onclick="deleteTextLine('.$textline['id'].'); return false;" title="Löschen">';
$html .= '<span class="fas fa-trash pictodelete" title="Löschen"></span></a>';
}
$html .= '</td>';
// Move (Spalte 13)
$html .= '<td class="linecolmove tdlineupdown center"></td>';
// Unlink (Spalte 14)
$html .= '<td class="linecolunlink"></td>';
$html .= '</tr>';
return $html;
}
/**
* Rendert eine Subtotal-Zeile
*/
private function renderSubtotalLine($subtotal)
{
global $db;
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
if (!$tables) return '';
// Hole in_facturedet Status
$sql_sync = "SELECT in_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE rowid = ".(int)$subtotal->rowid;
$res_sync = $db->query($sql_sync);
$obj_sync = $db->fetch_object($res_sync);
$in_facturedet = $obj_sync ? $obj_sync->in_facturedet : 0;
$sync_checked = $in_facturedet ? 'checked' : '';
$in_class = $in_facturedet ? ' in-facturedet' : '';
// Berechne aktuelle Summe
$sql = "SELECT SUM(d.total_ht) as total";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON d.rowid = m.".$tables['fk_line'];
$sql .= " WHERE m.parent_section = ".(int)$subtotal->parent_section;
$sql .= " AND m.".$tables['fk_parent']." = ".(int)$this->currentDocumentId;
$sql .= " AND m.document_type = '".$db->escape($this->currentDocType)."'";
$sql .= " AND m.line_type = 'product'";
$resql = $db->query($sql);
$sum = 0;
if ($resql && ($obj = $db->fetch_object($resql))) {
$sum = $obj->total ? $obj->total : 0;
}
$formatted = number_format($sum, 2, ',', '.');
$html = '<tr class="subtotal-row'.$in_class.'" data-section-id="'.$subtotal->parent_section.'" data-subtotal-id="'.$subtotal->rowid.'" data-line-order="'.$subtotal->line_order.'" style="font-weight:bold;">';
$html .= '<td colspan="9" style="text-align:right; padding:8px;">';
$html .= 'Zwischensumme:';
// Sync-Checkbox (NEU!) - nur im Entwurfsstatus
if ($this->isDraft) {
$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="'.$subtotal->rowid.'" data-line-type="subtotal" '.$sync_checked.' onclick="toggleFacturedetSync('.$subtotal->rowid.', \'subtotal\', this);">';
$html .= ' 📄</label>';
}
$html .= '</td>';
$html .= '<td class="linecolht right" style="padding:8px;">'.$formatted.'</td>';
$html .= '<td class="linecoledit"></td>';
$html .= '<td class="linecoldelete"></td>';
$html .= '<td class="linecolmove"></td>';
$html .= '<td class="linecolunlink"></td>';
$html .= '</tr>';
return $html;
}
/**
* Hook: Wird aufgerufen wenn das Produkt-Hinzufügen-Formular gerendert wird
* Fügt Section-Dropdown hinzu UND rendert trailing items (Textzeilen, leere Sections)
*/
public function formAddObjectLine($parameters, &$object, &$action, $hookmanager)
{
global $db;
// Erkenne Dokumenttyp
$docType = DocumentTypeHelper::getTypeFromObject($object);
if (!$docType) {
return 0;
}
// Prüfe Context
$expectedContext = DocumentTypeHelper::getContext($docType);
if ($parameters['currentcontext'] != $expectedContext) {
return 0;
}
$document_id = $object->id;
$tables = DocumentTypeHelper::getTableNames($docType);
$doc_key = $docType.'_'.$document_id;
// Initialisiere rendered_sections falls nicht vorhanden
if (!isset(self::$rendered_sections[$doc_key])) {
self::$rendered_sections[$doc_key] = array();
}
// ========== TRAILING ITEMS RENDERN ==========
// Hole ALLE Manager-Einträge, die NACH der letzten facturedet-Zeile kommen
// und noch nicht gerendert wurden
// Finde die höchste line_order einer Zeile, die in facturedet ist
$sql_max_rendered = "SELECT MAX(m.line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql_max_rendered .= " WHERE m.".$tables['fk_parent']." = ".(int)$document_id;
$sql_max_rendered .= " AND m.document_type = '".$db->escape($docType)."'";
$sql_max_rendered .= " AND m.".$tables['fk_line']." IS NOT NULL";
$res_max = $db->query($sql_max_rendered);
$obj_max = $db->fetch_object($res_max);
$last_rendered_order = $obj_max && $obj_max->max_order ? (int)$obj_max->max_order : 0;
if ($this->debug) {
error_log('[SubtotalTitle] formAddObjectLine: Letzte gerenderte line_order = '.$last_rendered_order);
}
// Hole alle trailing items (alles mit line_order > last_rendered_order)
$sql_trailing = "SELECT rowid, line_type, title, line_order, parent_section, show_subtotal, collapsed, in_facturedet";
$sql_trailing .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_trailing .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_trailing .= " AND document_type = '".$db->escape($docType)."'";
$sql_trailing .= " AND line_order > ".$last_rendered_order;
$sql_trailing .= " ORDER BY line_order";
$resql_trailing = $db->query($sql_trailing);
while ($trailing = $db->fetch_object($resql_trailing)) {
if ($trailing->line_type == 'section') {
// Section-Header rendern (falls noch nicht gerendert)
if (!in_array($trailing->rowid, self::$rendered_sections[$doc_key])) {
$section_data = array(
'section_id' => $trailing->rowid,
'title' => $trailing->title,
'show_subtotal' => $trailing->show_subtotal,
'collapsed' => $trailing->collapsed,
'in_facturedet' => $trailing->in_facturedet
);
echo $this->renderSectionHeader($section_data);
self::$rendered_sections[$doc_key][] = $trailing->rowid;
if ($this->debug) {
error_log('[SubtotalTitle] ✅ Trailing Section "'.$trailing->title.'" gerendert (order='.$trailing->line_order.')');
}
// Falls Subtotal aktiviert, auch Subtotal rendern
if ($trailing->show_subtotal) {
$sql_sub = "SELECT rowid, title, parent_section, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_sub .= " WHERE parent_section = ".(int)$trailing->rowid;
$sql_sub .= " AND line_type = 'subtotal'";
$sql_sub .= " AND document_type = '".$db->escape($docType)."'";
$resql_sub = $db->query($sql_sub);
if ($obj_sub = $db->fetch_object($resql_sub)) {
$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] ✅ Trailing Subtotal für Section "'.$trailing->title.'" gerendert');
}
}
}
}
}
} elseif ($trailing->line_type == 'text') {
// Textzeile rendern
$text_key = 'text_'.$trailing->rowid;
if (!in_array($text_key, self::$rendered_sections[$doc_key])) {
$textline = array(
'id' => $trailing->rowid,
'title' => $trailing->title,
'parent_section' => $trailing->parent_section,
'line_order' => $trailing->line_order,
'in_facturedet' => $trailing->in_facturedet
);
echo $this->renderTextLine($textline);
self::$rendered_sections[$doc_key][] = $text_key;
if ($this->debug) {
error_log('[SubtotalTitle] ✅ Trailing Textzeile "'.$trailing->title.'" gerendert (order='.$trailing->line_order.')');
}
}
} elseif ($trailing->line_type == 'subtotal') {
// Subtotal rendern (falls nicht schon mit Section gerendert)
$subtotal_key = 'subtotal_'.$trailing->rowid;
if (!in_array($subtotal_key, self::$rendered_sections[$doc_key])) {
echo $this->renderSubtotalLine($trailing);
self::$rendered_sections[$doc_key][] = $subtotal_key;
if ($this->debug) {
error_log('[SubtotalTitle] ✅ Trailing Subtotal gerendert (order='.$trailing->line_order.')');
}
}
}
}
// Dann das Section-Dropdown rendern
echo $this->renderSectionDropdown($document_id, $docType);
return 0;
}
/**
* Synchronisiert llx_facture_lines_manager mit vorhandenen Zeilen
*/
private function syncManagerTable($document_id, $docType)
{
global $db;
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) return;
// 1. CLEANUP: Lösche verwaiste Einträge
$sql_cleanup = "DELETE m FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql_cleanup .= " LEFT JOIN ".MAIN_DB_PREFIX.$tables['lines_table']." d ON m.".$tables['fk_line']." = d.rowid";
$sql_cleanup .= " WHERE m.".$tables['fk_parent']." = ".(int)$document_id;
$sql_cleanup .= " AND m.line_type = 'product'";
$sql_cleanup .= " AND d.rowid IS NULL";
$result = $db->query($sql_cleanup);
// 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) {
// Neues Produkt gefunden - merken mit rang
$new_products[] = array('rowid' => $obj->rowid, 'rang' => $obj->rang);
}
}
// 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)$product['rowid'].", ".$next_order.", NOW())";
$db->query($sql_ins);
$next_order++; // Für jedes weitere Produkt erhöhen
}
}
}
/**
* Holt die Section für eine Produktzeile
*/
private function getSectionForLine($line_id)
{
global $db;
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
if (!$tables) return null;
$sql = "SELECT m.parent_section as section_id, s.title, s.show_subtotal, s.collapsed";
$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.".$tables['fk_line']." = ".(int)$line_id;
$sql .= " AND m.document_type = '".$db->escape($this->currentDocType)."'";
$sql .= " AND m.line_type = 'product'";
$resql = $db->query($sql);
if ($obj = $db->fetch_object($resql)) {
if ($obj->section_id) {
return array(
'section_id' => $obj->section_id,
'title' => $obj->title,
'show_subtotal' => $obj->show_subtotal,
'collapsed' => $obj->collapsed
);
}
}
return null;
}
/**
* Prüft ob dies die erste Zeile einer Section ist
*/
private function isFirstLineInSection($line_id, $section_id)
{
global $db;
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
if (!$tables) return false;
$sql = "SELECT line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_line']." = ".(int)$line_id;
$sql .= " AND document_type = '".$db->escape($this->currentDocType)."'";
$resql = $db->query($sql);
if (!$resql || !($obj = $db->fetch_object($resql))) {
return false;
}
$current_order = $obj->line_order;
$sql = "SELECT MIN(line_order) as min_order";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE parent_section = ".(int)$section_id;
$sql .= " AND line_type = 'product'";
$resql = $db->query($sql);
if (!$resql || !($obj = $db->fetch_object($resql))) {
return false;
}
return ($current_order == $obj->min_order);
}
/**
* Rendert einen Section-Header
*/
private function renderSectionHeader($section)
{
global $db;
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
if (!$tables) return '';
// Hole line_order und in_facturedet der Section
$sql = "SELECT line_order, in_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE rowid = ".(int)$section['section_id'];
$resql = $db->query($sql);
$line_order = 0;
$in_facturedet = 0;
if ($obj = $db->fetch_object($resql)) {
$line_order = $obj->line_order;
$in_facturedet = $obj->in_facturedet;
}
$sync_checked = $in_facturedet ? 'checked' : '';
$in_class = $in_facturedet ? ' in-facturedet' : '';
// Hole Produkte dieser Section (IDs + Anzahl)
$sql_products = "SELECT ".$tables['fk_line']." FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_products .= " WHERE parent_section = ".(int)$section['section_id'];
$sql_products .= " AND ".$tables['fk_parent']." = ".(int)$this->currentDocumentId;
$sql_products .= " AND document_type = '".$db->escape($this->currentDocType)."'";
$sql_products .= " AND line_type = 'product'";
$res_products = $db->query($sql_products);
$product_ids = [];
while ($obj_prod = $db->fetch_object($res_products)) {
$fk_line_col = $tables['fk_line'];
$product_ids[] = (int)$obj_prod->$fk_line_col;
}
$product_count = count($product_ids);
$product_ids_json = htmlspecialchars(json_encode($product_ids));
$html = '<tr class="section-header'.$in_class.'" data-section-id="'.$section['section_id'].'" data-section-title="'.htmlspecialchars($section['title']).'" data-line-order="'.$line_order.'" data-product-count="'.$product_count.'" data-product-ids="'.$product_ids_json.'">';
// Titel (colspan=10)
$html .= '<td colspan="6" style="font-weight:bold; padding:8px;">';
$html .= '<span class="section-toggle" onclick="toggleSection('.$section['section_id'].'); event.stopPropagation();">▼</span>';
$html .= htmlspecialchars($section['title']);
$html .= '<span class="section-count">'.$product_count.' Produkte</span></td><td colspan="2" align="right">';
// Zwischensummen-Checkbox - nur im Entwurfsstatus
if ($this->isDraft) {
$checked = $section['show_subtotal'] ? 'checked' : '';
$html .= ' <label style="margin-left:15px;font-weight:normal;font-size:12px;" onclick="event.stopPropagation();">';
$html .= '<input type="checkbox" class="subtotal-toggle" data-section-id="'.$section['section_id'].'" '.$checked.' onclick="event.stopPropagation(); toggleSubtotal('.$section['section_id'].', this);">';
$html .= ' <b>€</b></label>';
// Sync-Checkbox (NEU!)
$html .= ' <label style="margin-left:15px;font-weight:normal;font-size:12px;" onclick="event.stopPropagation();" title="In Rechnung/PDF anzeigen">';
$html .= '<input type="checkbox" class="sync-checkbox" data-line-id="'.$section['section_id'].'" data-line-type="section" '.$sync_checked.' onclick="event.stopPropagation(); toggleFacturedetSync('.$section['section_id'].', \'section\', this);">';
$html .= ' 📄</label>';
}
$html .= '</td><td colspan="2" align="right">';
// Move-Buttons - nur im Entwurfsstatus
if ($this->isDraft) {
$html .= ' <a href="#" onclick="moveSection('.$section['section_id'].', \'up\'); return false;" title="Nach oben"><span class="fa fa-long-arrow-alt-up"></span></a>';
$html .= ' <a href="#" onclick="moveSection('.$section['section_id'].', \'down\'); return false;" title="Nach unten"><span class="fa fa-long-arrow-alt-down"></span></a>';
}
$html .= '</td>';
// Edit (Spalte 11) - nur im Entwurfsstatus
$html .= '<td class="linecoledit center">';
if ($this->isDraft) {
$html .= '<a href="#" onclick="renameSection('.$section['section_id'].', \''.addslashes($section['title']).'\'); return false;" title="Umbenennen">';
$html .= '<span class="fas fa-pencil-alt" style="color:#444;" title="Ändern"></span></a>';
}
$html .= '</td>';
// Delete (Spalte 12) - nur im Entwurfsstatus
$html .= '<td class="linecoldelete center">';
if ($this->isDraft) {
if ($product_count == 0) {
$html .= '<a href="#" onclick="deleteSection('.$section['section_id'].'); return false;" title="Leere Gruppe löschen">';
$html .= '<span class="fas fa-trash pictodelete" title="Löschen"></span></a>';
} else {
$html .= '<a href="#" onclick="deleteSectionForce('.$section['section_id'].'); return false;" title="⚠️ Gruppe MIT '.$product_count.' Produkten löschen">';
$html .= '<span class="fas fa-trash" style="color:#c00;" title="Alles löschen"></span></a>';
}
}
$html .= '</td>';
// Move (Spalte 13)
$html .= '<td class="linecolmove center"></td>';
// Unlink (Spalte 14)
$html .= '<td class="linecolunlink"></td>';
$html .= '</tr>';
return $html;
}
/**
* Rendert Section-Dropdown im Add-Product-Formular
*/
private function renderSectionDropdown($document_id, $docType)
{
global $db;
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) return '';
$sql = "SELECT rowid, title FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'section'";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$html = '<script>';
$html .= '$(document).ready(function() {';
// Debug: Zeige Anzahl gefundener Sections
$num_sections = $resql ? $db->num_rows($resql) : 0;
$html .= ' console.log("[SubtotalTitle] Gefundene Produktgruppen: '.$num_sections.'");';
$html .= ' console.log("[SubtotalTitle] Dokumenttyp: '.$docType.'");';
$html .= ' console.log("[SubtotalTitle] Dokument ID: '.$document_id.'");';
$html .= ' var dropdown = \'<tr><td colspan="13">Produktgruppe: <select name="section_id_dropdown" id="section_id_dropdown" style="margin-left:5px;">\';';
$html .= ' dropdown += \'<option value="">-- Keine Produktgruppe --</option>\';';
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$safe_title = str_replace(array("'", '"', "\n", "\r"), array("\\'", '&quot;', ' ', ' '), $obj->title);
$html .= ' console.log("[SubtotalTitle] Section gefunden: '.$obj->rowid.' - '.$safe_title.'");';
$html .= ' dropdown += \'<option value="'.$obj->rowid.'">'.addslashes($obj->title).'</option>\';';
}
} else {
$safe_error = str_replace(array("'", '"'), array("\\'", '&quot;'), $db->lasterror());
$html .= ' console.error("[SubtotalTitle] SQL Error: '.$safe_error.'");';
}
$html .= ' dropdown += \'</select></td></tr>\';';
$html .= ' $(\'#addproduct tr:last\').before(dropdown);';
$html .= ' $(\'form[name="addproduct"]\').append(\'<input type="hidden" name="section_id" id="section_id" value="">\');';
// Lade gespeicherte Auswahl aus sessionStorage (dokumentspezifisch)
$html .= ' var storageKey = \'subtotaltitle_section_\' + '.$document_id.';';
$html .= ' var savedSection = sessionStorage.getItem(storageKey);';
$html .= ' if (savedSection) {';
$html .= ' $(\'#section_id_dropdown\').val(savedSection);';
$html .= ' $(\'#section_id\').val(savedSection);';
$html .= ' console.log(\'[SubtotalTitle] Gespeicherte Section geladen:\', savedSection);';
$html .= ' }';
$html .= ' $(document).on(\'change\', \'#section_id_dropdown\', function() {';
$html .= ' var val = $(this).val();';
$html .= ' console.log(\'Section selected:\', val);';
$html .= ' $(\'#section_id\').val(val);';
// Speichere Auswahl in sessionStorage (dokumentspezifisch)
$html .= ' if (val) {';
$html .= ' sessionStorage.setItem(storageKey, val);';
$html .= ' } else {';
$html .= ' sessionStorage.removeItem(storageKey);';
$html .= ' }';
$html .= ' });';
$html .= '});';
$html .= '</script>';
return $html;
}
/**
* Fügt ein Produkt zu einer Section hinzu
*/
private function addProductToSection($doc_key, $line_id, $section_id)
{
global $db;
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
if (!$tables) return;
if ($this->debug) {
error_log('[SubtotalTitle] 🔴 addProductToSection: doc='.$doc_key.', line='.$line_id.', section='.$section_id);
}
$sql_check = "SELECT rowid, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_check .= " WHERE ".$tables['fk_line']." = ".(int)$line_id;
$sql_check .= " AND document_type = '".$db->escape($this->currentDocType)."'";
$resql_check = $db->query($sql_check);
if ($db->num_rows($resql_check) > 0) {
$obj = $db->fetch_object($resql_check);
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_upd .= " SET parent_section = ".(int)$section_id;
$sql_upd .= " WHERE rowid = ".(int)$obj->rowid;
$db->query($sql_upd);
$this->reorderLines($doc_key);
} else {
$sql = "SELECT line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE rowid = ".(int)$section_id;
$resql = $db->query($sql);
if (!$resql || !($obj = $db->fetch_object($resql))) {
return;
}
$section_order = $obj->line_order;
$new_order = $section_order + 1;
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " SET line_order = line_order + 1";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_key;
$sql .= " AND document_type = '".$db->escape($this->currentDocType)."'";
$sql .= " AND line_order >= ".$new_order;
$db->query($sql);
$sql = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " (".$tables['fk_parent'].", document_type, line_type, ".$tables['fk_line'].", parent_section, line_order, date_creation)";
$sql .= " VALUES (".(int)$doc_key.", '".$db->escape($this->currentDocType)."', 'product', ".(int)$line_id.", ".(int)$section_id.", ".$new_order.", NOW())";
$db->query($sql);
}
}
/**
* Fügt ein freies Produkt hinzu
*/
private function addFreeProduct($doc_key, $line_id)
{
global $db;
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
if (!$tables) return;
if ($this->debug) {
error_log('[SubtotalTitle] 🟢 addFreeProduct: doc='.$doc_key.', line='.$line_id.' (parent_section=NULL)');
}
$next_order = $this->getNextLineOrder($doc_key, $this->currentDocType);
$sql = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " (".$tables['fk_parent'].", document_type, line_type, ".$tables['fk_line'].", line_order, date_creation)";
$sql .= " VALUES (".(int)$doc_key.", '".$db->escape($this->currentDocType)."', 'product', ".(int)$line_id.", ".$next_order.", NOW())";
$db->query($sql);
}
/**
* Synchronisiert lines_table.rang aus line_order
*/
private function syncRangFromManager($doc_key)
{
global $db;
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
if (!$tables) return;
$sql = "SELECT ".$tables['fk_line'].", line_order";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_key;
$sql .= " AND document_type = '".$db->escape($this->currentDocType)."'";
$sql .= " AND line_type = 'product'";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$rang = 1;
$fk_line_col = $tables['fk_line'];
while ($obj = $db->fetch_object($resql)) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_upd .= " SET rang = ".$rang;
$sql_upd .= " WHERE rowid = ".(int)$obj->$fk_line_col;
$db->query($sql_upd);
$rang++;
}
}
/**
* Holt die nächste freie line_order
*/
private function getNextLineOrder($document_id, $docType)
{
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 .= " 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;
}
return ($obj->max_order ? $obj->max_order + 1 : 1);
}
/**
* Holt die rowid der letzten Produktzeile
*/
private function getLastProductLine($doc_key)
{
global $db;
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
if (!$tables) return 0;
$fk_line_col = $tables['fk_line'];
$sql = "SELECT m.".$fk_line_col;
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
$sql .= " WHERE m.".$tables['fk_parent']." = ".(int)$doc_key;
$sql .= " AND m.document_type = '".$db->escape($this->currentDocType)."'";
$sql .= " AND m.line_type = 'product'";
$sql .= " ORDER BY m.line_order DESC";
$sql .= " LIMIT 1";
$resql = $db->query($sql);
if ($obj = $db->fetch_object($resql)) {
return $obj->$fk_line_col;
}
return 0;
}
/**
* Holt alle Sections die keine Produkte haben
*/
private function getEmptySections($doc_key)
{
global $db;
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
if (!$tables) return array();
$sql = "SELECT s.rowid as section_id, s.title, s.show_subtotal, s.collapsed";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager s";
$sql .= " WHERE s.".$tables['fk_parent']." = ".(int)$doc_key;
$sql .= " AND s.document_type = '".$db->escape($this->currentDocType)."'";
$sql .= " AND s.line_type = 'section'";
$sql .= " AND NOT EXISTS (";
$sql .= " SELECT 1 FROM ".MAIN_DB_PREFIX."facture_lines_manager p";
$sql .= " WHERE p.parent_section = s.rowid";
$sql .= " AND p.".$tables['fk_parent']." = ".(int)$doc_key;
$sql .= " AND p.document_type = '".$db->escape($this->currentDocType)."'";
$sql .= " )";
$sql .= " ORDER BY s.line_order";
$resql = $db->query($sql);
$sections = array();
while ($obj = $db->fetch_object($resql)) {
$sections[] = array(
'section_id' => $obj->section_id,
'title' => $obj->title,
'show_subtotal' => $obj->show_subtotal,
'collapsed' => $obj->collapsed
);
}
return $sections;
}
/**
* Sortiert alle Zeilen neu: Schließt nur Lücken, behält Reihenfolge bei
*/
private function reorderLines($doc_key)
{
global $db;
$tables = DocumentTypeHelper::getTableNames($this->currentDocType);
if (!$tables) return;
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_key;
$sql .= " AND document_type = '".$db->escape($this->currentDocType)."'";
$sql .= " ORDER BY line_order";
$resql = $db->query($sql);
$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++;
}
}
}