1728 lines
80 KiB
PHP
Executable file
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 . '&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("\\'", '"', ' ', ' '), $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("\\'", '"'), $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++;
|
|
}
|
|
}
|
|
}
|