* Copyright (C) 2026 Eduard Wisch * * 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 . */ /** * \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'; /** * 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(); /** * @var DoliDB Database handler. */ public $db; /** * @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; if (in_array('invoicecard', explode(':', $parameters['currentcontext']))) { // Lade Übersetzungen $langs->load('subtotaltitle@subtotaltitle'); // CSS $cssPath = dol_buildpath('/custom/subtotaltitle/css/subtotaltitle.css', 1); echo ''."\n"; // Übersetzungen als JavaScript-Variablen bereitstellen echo ''."\n"; // Haupt-JavaScript $jsPath = dol_buildpath('/custom/subtotaltitle/js/subtotaltitle.js', 1); echo ''."\n"; // Sync-JavaScript (NEU!) $jsSyncPath = dol_buildpath('/custom/subtotaltitle/js/subtotaltitle_sync.js', 1); echo ''."\n"; // Textzeile-Button echo ''."\n"; // Massenlösch-Button (ans ENDE der Hauptzeile) - NUR EINMAL EINFÜGEN echo ''."\n"; // Sync-Buttons + Collapse-Buttons - rechts ausgerichtet echo ''."\n"; } return 0; } /** * Execute action */ public function getNomUrl($parameters, &$object, &$action) { global $db, $langs, $conf, $user; $this->resprints = ''; return 0; } /** * Overload the doActions function */ public function doActions($parameters, &$object, &$action, $hookmanager) { 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 = ''; } 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] .= '' . $datacount . ''; } $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; if ($parameters['currentcontext'] != 'invoicecard') { return 0; } if (!is_object($object) || $object->element != 'facture') { return 0; } $facture_id = $object->id; $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 ist eine unserer speziellen Zeilen - per JS ausblenden echo ''; return 0; } // Rechnung neu laden um aktuelle rang zu haben static $reloaded = array(); if (!isset($reloaded[$facture_id])) { $object->fetch($facture_id); $object->fetch_thirdparty(); $object->fetch_lines(); $reloaded[$facture_id] = true; } // Synchronisiere Manager-Tabelle $this->syncManagerTable($facture_id); // Rendere alle Sections die VOR dieser Zeile kommen sollten (inkl. leere!) $this->renderAllPendingSections($facture_id, $line); // WICHTIG: Markiere diese Zeile mit line_order UND parent_section für JavaScript $sql = "SELECT line_order, parent_section FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql .= " WHERE fk_facturedet = ".(int)$line->id; $resql = $db->query($sql); if ($obj = $db->fetch_object($resql)) { $parentSection = $obj->parent_section ? $obj->parent_section : 'null'; echo ''; } return 0; } /** * Rendert ALLE Sections die VOR dieser RANG-Position kommen sollten */ private function renderAllPendingSections($facture_id, $line) { global $db; static $last_rang = array(); static $last_parent_section = array(); if (!isset(self::$rendered_sections[$facture_id])) { self::$rendered_sections[$facture_id] = array(); $last_rang[$facture_id] = 0; $last_parent_section[$facture_id] = null; } // Hole rang dieser Produktzeile aus facturedet $sql = "SELECT rang FROM ".MAIN_DB_PREFIX."facturedet"; $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 fk_facturedet = ".(int)$line->id; $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) if ($last_parent_section[$facture_id] && $last_parent_section[$facture_id] != $current_parent_section) { $sql_subtotal = "SELECT rowid, title, parent_section, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql_subtotal .= " WHERE parent_section = ".(int)$last_parent_section[$facture_id]; $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[$facture_id])) { echo $this->renderSubtotalLine($obj_sub); self::$rendered_sections[$facture_id][] = $subtotal_key; if ($this->debug) { error_log('[SubtotalTitle] ✅ Subtotal "'.$obj_sub->title.'" gerendert (Section-Wechsel)'); } } } } // Hole ALLE Sections ZWISCHEN letztem rang und aktuellem rang $sql = "SELECT DISTINCT s.rowid, s.title, s.show_subtotal, s.collapsed, s.line_order, s.in_facturedet,"; $sql .= " (SELECT MIN(fd.rang) FROM ".MAIN_DB_PREFIX."facture_lines_manager m2"; $sql .= " INNER JOIN ".MAIN_DB_PREFIX."facturedet fd ON fd.rowid = m2.fk_facturedet"; $sql .= " WHERE m2.parent_section = s.rowid) as first_product_rang"; $sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager s"; $sql .= " WHERE s.fk_facture = ".(int)$facture_id; $sql .= " AND s.line_type = 'section'"; $sql .= " HAVING first_product_rang > ".(int)$last_rang[$facture_id]; $sql .= " AND first_product_rang <= ".(int)$current_rang; $sql .= " ORDER BY first_product_rang"; $resql = $db->query($sql); while ($obj = $db->fetch_object($resql)) { if (!in_array($obj->rowid, self::$rendered_sections[$facture_id])) { $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[$facture_id][] = $obj->rowid; if ($this->debug) { error_log('[SubtotalTitle] ✅ Section "'.$obj->title.'" gerendert vor rang '.$current_rang); } } } // Rendere Textzeilen die VOR dieser Produktzeile kommen $sql_text = "SELECT rowid, title, line_order, in_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql_text .= " WHERE fk_facture = ".(int)$facture_id; $sql_text .= " AND line_type = 'text'"; $sql_text .= " AND line_order < ".(int)$current_line_order; $sql_text .= " ORDER BY line_order"; $resql_text = $db->query($sql_text); while ($obj_text = $db->fetch_object($resql_text)) { $text_key = 'text_'.$obj_text->rowid; if (!in_array($text_key, self::$rendered_sections[$facture_id])) { $textline = array( 'id' => $obj_text->rowid, 'title' => $obj_text->title, 'line_order' => $obj_text->line_order, 'in_facturedet' => $obj_text->in_facturedet ); echo $this->renderTextLine($textline); self::$rendered_sections[$facture_id][] = $text_key; if ($this->debug) { error_log('[SubtotalTitle] ✅ Textzeile "'.$obj_text->title.'" gerendert'); } } } // Merke für nächsten Durchlauf $last_rang[$facture_id] = $current_rang; $last_parent_section[$facture_id] = $current_parent_section; } /** * 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 = ''; // Inhalt (colspan=10) $html .= ''; $html .= htmlspecialchars($textline['title']); // Sync-Checkbox (NEU!) $html .= ' '; $html .= ''; // Edit (Spalte 11) $html .= ''; $html .= ''; $html .= ''; $html .= ''; // Delete (Spalte 12) $html .= ''; $html .= ''; $html .= ''; $html .= ''; // Move (Spalte 13) $html .= ''; // Unlink (Spalte 14) $html .= ''; $html .= ''; return $html; } /** * Rendert eine Subtotal-Zeile */ private function renderSubtotalLine($subtotal) { global $db; // 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."facturedet d ON d.rowid = m.fk_facturedet"; $sql .= " WHERE m.parent_section = ".(int)$subtotal->parent_section; $sql .= " AND m.line_type = 'product'"; $resql = $db->query($sql); $obj = $db->fetch_object($resql); $sum = $obj->total ? $obj->total : 0; $formatted = number_format($sum, 2, ',', '.'); $html = ''; $html .= ''; $html .= 'Zwischensumme:'; // Sync-Checkbox (NEU!) $html .= ' '; $html .= ''; $html .= ''.$formatted.''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; return $html; } /** * Hook: Wird aufgerufen wenn das Produkt-Hinzufügen-Formular gerendert wird * Fügt Section-Dropdown hinzu */ public function formAddObjectLine($parameters, &$object, &$action, $hookmanager) { global $db; if ($parameters['currentcontext'] != 'invoicecard') { return 0; } if (!is_object($object) || $object->element != 'facture') { return 0; } $facture_id = $object->id; echo $this->renderSectionDropdown($facture_id); return 0; } /** * Synchronisiert llx_facture_lines_manager mit vorhandenen facturedet-Zeilen */ private function syncManagerTable($facture_id) { global $db; // 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."facturedet d ON m.fk_facturedet = d.rowid"; $sql_cleanup .= " WHERE m.fk_facture = ".(int)$facture_id; $sql_cleanup .= " AND m.line_type = 'product'"; $sql_cleanup .= " AND d.rowid IS NULL"; $result = $db->query($sql_cleanup); /*if ($result && $db->affected_rows > 0 && $this->debug) { error_log('[SubtotalTitle] 🧹 Cleanup: '.$db->affected_rows.' verwaiste Einträge entfernt für Facture '.$facture_id); }*/ // 2. Hole alle Produktzeilen der Rechnung $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facturedet"; $sql .= " WHERE fk_facture = ".(int)$facture_id; $sql .= " ORDER BY rang"; $resql = $db->query($sql); while ($obj = $db->fetch_object($resql)) { $sql_check = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql_check .= " WHERE fk_facturedet = ".(int)$obj->rowid; $resql_check = $db->query($sql_check); if ($db->num_rows($resql_check) == 0) { $next_order = $this->getNextLineOrder($facture_id); $sql_ins = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager"; $sql_ins .= " (fk_facture, line_type, fk_facturedet, line_order, date_creation)"; $sql_ins .= " VALUES (".(int)$facture_id.", 'product', ".(int)$obj->rowid.", ".$next_order.", NOW())"; $db->query($sql_ins); } } } /** * Holt die Section für eine Produktzeile */ private function getSectionForLine($line_id) { global $db; $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.fk_facturedet = ".(int)$line_id; $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; $sql = "SELECT line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql .= " WHERE fk_facturedet = ".(int)$line_id; $resql = $db->query($sql); $obj = $db->fetch_object($resql); $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); $obj = $db->fetch_object($resql); return ($current_order == $obj->min_order); } /** * Rendert einen Section-Header */ private function renderSectionHeader($section) { global $db; // 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 fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql_products .= " WHERE parent_section = ".(int)$section['section_id']; $sql_products .= " AND line_type = 'product'"; $res_products = $db->query($sql_products); $product_ids = []; while ($obj_prod = $db->fetch_object($res_products)) { $product_ids[] = (int)$obj_prod->fk_facturedet; } $product_count = count($product_ids); $product_ids_json = htmlspecialchars(json_encode($product_ids)); $html = ''; // Titel (colspan=10) $html .= ''; $html .= ''; $html .= htmlspecialchars($section['title']); $html .= ''.$product_count.' Produkte'; // Zwischensummen-Checkbox $checked = $section['show_subtotal'] ? 'checked' : ''; $html .= ' '; // Sync-Checkbox (NEU!) $html .= ' '; $html .= ''; $html .= ' '; $html .= ' '; $html .= ''; // Edit (Spalte 11) $html .= ''; $html .= ''; $html .= ''; $html .= ''; // Delete (Spalte 12) $html .= ''; if ($product_count == 0) { $html .= ''; $html .= ''; } else { $html .= ''; $html .= ''; } $html .= ''; // Move (Spalte 13) $html .= ''; // Unlink (Spalte 14) $html .= ''; $html .= ''; return $html; } /** * Rendert Section-Dropdown im Add-Product-Formular */ private function renderSectionDropdown($facture_id) { global $db; $sql = "SELECT rowid, title FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql .= " WHERE fk_facture = ".(int)$facture_id; $sql .= " AND line_type = 'section'"; $sql .= " ORDER BY line_order"; $resql = $db->query($sql); $html = ''; return $html; } /** * Fügt ein Produkt zu einer Section hinzu */ private function addProductToSection($facture_id, $line_id, $section_id) { global $db; if ($this->debug) { error_log('[SubtotalTitle] 🔴 addProductToSection: facture='.$facture_id.', line='.$line_id.', section='.$section_id); } $sql_check = "SELECT rowid, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql_check .= " WHERE fk_facturedet = ".(int)$line_id; $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($facture_id); } else { $sql = "SELECT line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql .= " WHERE rowid = ".(int)$section_id; $resql = $db->query($sql); $obj = $db->fetch_object($resql); $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 fk_facture = ".(int)$facture_id; $sql .= " AND line_order >= ".$new_order; $db->query($sql); $sql = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager"; $sql .= " (fk_facture, line_type, fk_facturedet, parent_section, line_order, date_creation)"; $sql .= " VALUES (".(int)$facture_id.", 'product', ".(int)$line_id.", ".(int)$section_id.", ".$new_order.", NOW())"; $db->query($sql); } } /** * Fügt ein freies Produkt hinzu */ private function addFreeProduct($facture_id, $line_id) { global $db; if ($this->debug) { error_log('[SubtotalTitle] 🟢 addFreeProduct: facture='.$facture_id.', line='.$line_id.' (parent_section=NULL)'); } $next_order = $this->getNextLineOrder($facture_id); $sql = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager"; $sql .= " (fk_facture, line_type, fk_facturedet, line_order, date_creation)"; $sql .= " VALUES (".(int)$facture_id.", 'product', ".(int)$line_id.", ".$next_order.", NOW())"; $db->query($sql); } /** * Synchronisiert facturedet.rang aus line_order */ private function syncRangFromManager($facture_id) { global $db; $sql = "SELECT fk_facturedet, line_order"; $sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql .= " WHERE fk_facture = ".(int)$facture_id; $sql .= " AND line_type = 'product'"; $sql .= " ORDER BY line_order"; $resql = $db->query($sql); $rang = 1; while ($obj = $db->fetch_object($resql)) { $sql_upd = "UPDATE ".MAIN_DB_PREFIX."facturedet"; $sql_upd .= " SET rang = ".$rang; $sql_upd .= " WHERE rowid = ".(int)$obj->fk_facturedet; $db->query($sql_upd); $rang++; } } /** * Holt die nächste freie line_order */ private function getNextLineOrder($facture_id) { global $db; $sql = "SELECT MAX(line_order) as max_order"; $sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql .= " WHERE fk_facture = ".(int)$facture_id; $resql = $db->query($sql); $obj = $db->fetch_object($resql); return ($obj->max_order ? $obj->max_order + 1 : 1); } /** * Holt die rowid der letzten Produktzeile */ private function getLastProductLine($facture_id) { global $db; $sql = "SELECT m.fk_facturedet"; $sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m"; $sql .= " WHERE m.fk_facture = ".(int)$facture_id; $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_facturedet; } return 0; } /** * Holt alle Sections die keine Produkte haben */ private function getEmptySections($facture_id) { global $db; $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.fk_facture = ".(int)$facture_id; $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 .= " )"; $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($facture_id) { global $db; $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql .= " WHERE fk_facture = ".(int)$facture_id; $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++; } } }