* * Stundenzettel - Auftrags-Integration * Zeigt Produktliste aus Auftrag mit Übernahme-Möglichkeit */ // Load Dolibarr environment $res = 0; if (!$res && file_exists("../main.inc.php")) $res = @include "../main.inc.php"; if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php"; if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php"; if (!$res) die("Include of main fails"); require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/order.lib.php'; require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php'; dol_include_once('/stundenzettel/class/stundenzettel.class.php'); dol_include_once('/stundenzettel/lib/stundenzettel.lib.php'); // Load translation files $langs->loadLangs(array("stundenzettel@stundenzettel", "orders", "products")); /** * Formatiert Mengen: Ganzzahlen ohne Dezimalstellen, sonst max. 2 Stellen * @param float $qty Menge * @return string Formatierte Menge */ function formatQty($qty) { $qty = (float)$qty; if ($qty == floor($qty)) { return number_format($qty, 0, ',', '.'); } // Runde auf 2 Stellen und entferne trailing zeros $formatted = rtrim(rtrim(number_format($qty, 2, ',', '.'), '0'), ','); return $formatted; } // Get parameters $id = GETPOST('id', 'int'); $action = GETPOST('action', 'aZ09'); $tab = GETPOST('tab', 'aZ09') ?: 'products'; $stundenzettel_id = GETPOST('stundenzettel_id', 'int'); $defaultFilter = getDolGlobalString('STUNDENZETTEL_DEFAULT_FILTER', 'open'); $filter = GETPOST('filter', 'alpha') ?: $defaultFilter; // Filter: all, open, done // Security check if (!$user->hasRight('stundenzettel', 'read')) { accessforbidden(); } // Load order $order = new Commande($db); if ($order->fetch($id) <= 0) { dol_print_error($db, 'Order not found'); exit; } // Berechtigung: Nur zugewiesener Benutzer oder Admin $canAccess = ($order->fk_user_author == $user->id || $user->admin || $user->hasRight('commande', 'lire')); if (!$canAccess) { accessforbidden('You are not assigned to this order'); } // Auto-Redirect: Wenn ein aktiver Stundenzettel für heute existiert, dorthin weiterleiten // Nur bei erstem Aufruf $noRedirect = GETPOST('noredirect', 'int'); if (!$noRedirect && empty($action)) { // Suche nach offenem Stundenzettel für diesen Auftrag (heute oder generell offen) $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel"; $sql .= " WHERE fk_commande = ".((int)$order->id); $sql .= " AND status = 0"; // Nur Entwürfe $sql .= " ORDER BY date_stundenzettel DESC, rowid DESC"; $sql .= " LIMIT 1"; $resql = $db->query($sql); if ($resql && $db->num_rows($resql) > 0) { $obj = $db->fetch_object($resql); // Weiterleitung zum gefundenen Stundenzettel header('Location: '.dol_buildpath('/stundenzettel/card.php?id='.$obj->rowid.'&tab=products', 1)); exit; } // Kein Stundenzettel vorhanden - bleibe auf dieser Seite um einen zu erstellen } /* * Actions */ // Produkte auf Stundenzettel übernehmen if ($action == 'transfer_products' && $user->hasRight('stundenzettel', 'write')) { // Prüfe ob Auftrag freigegeben ist $order->fetch_optionals(); $stzStatus = isset($order->array_options['options_stundenzettel_status']) ? (int)$order->array_options['options_stundenzettel_status'] : 0; if ($stzStatus >= 1) { setEventMessages($langs->trans('ErrorStundenzettelReleased'), null, 'errors'); header('Location: '.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1'); exit; } $selected = GETPOST('selected', 'array'); $selected_mehraufwand = GETPOST('selected_mehraufwand', 'array'); $date_stundenzettel = GETPOST('date_stundenzettel', 'alpha'); $target_stundenzettel_id = GETPOST('stundenzettel_id', 'int'); // Vorausgewählter Stundenzettel // Prüfe ob mindestens etwas ausgewählt wurde if (empty($selected) && empty($selected_mehraufwand)) { setEventMessages($langs->trans('NoProductsSelected'), null, 'errors'); } else { // Wenn nur Mehraufwand ausgewählt, $selected als leeres Array initialisieren if (empty($selected)) { $selected = array(); } // Mehraufwand-Zeilen werden direkt übernommen (ohne Bestätigungs-Dialog) // Produkte werden direkt übernommen (ohne Bestätigungs-Dialoge) $stundenzettel = new Stundenzettel($db); $use_stundenzettel_id = 0; $forceNewStundenzettel = GETPOST('force_new_stundenzettel', 'int'); // Wenn ein Stundenzettel vorgegeben wurde, prüfen ob er von heute ist if ($target_stundenzettel_id > 0 && !$forceNewStundenzettel) { if ($stundenzettel->fetch($target_stundenzettel_id) > 0) { // Prüfe ob der Stundenzettel von heute ist $stzDateStr = date('Y-m-d', $stundenzettel->date_stundenzettel); $todayStr = date('Y-m-d'); if ($stzDateStr == $todayStr) { // Stundenzettel ist von heute - verwenden $use_stundenzettel_id = $target_stundenzettel_id; } // Sonst: neuen für heute erstellen (weiter unten) } } // Wenn kein passender Stundenzettel, nach Datum suchen oder neu erstellen if ($use_stundenzettel_id <= 0) { // Bei force_new oder wenn Stundenzettel von anderem Tag: heutiges Datum verwenden $date = ($forceNewStundenzettel || !$date_stundenzettel) ? dol_now() : $date_stundenzettel; // Existierenden Stundenzettel für diesen Tag suchen oder neuen erstellen $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel"; $sql .= " WHERE fk_commande = ".((int)$order->id); $sql .= " AND date_stundenzettel = '".$db->idate($date)."'"; $sql .= " AND status = 0"; // Nur Entwürfe $resql = $db->query($sql); if ($resql && $db->num_rows($resql) > 0) { $obj = $db->fetch_object($resql); $use_stundenzettel_id = $obj->rowid; $stundenzettel->fetch($use_stundenzettel_id); } else { // Neuen erstellen $stundenzettel->fk_commande = $order->id; $stundenzettel->fk_soc = $order->socid; $stundenzettel->date_stundenzettel = $date; $use_stundenzettel_id = $stundenzettel->create($user); } } if ($use_stundenzettel_id > 0) { // Produkte hinzufügen foreach ($selected as $line_id) { // Hole Zeile aus commandedet $sql = "SELECT cd.rowid, cd.fk_product, cd.qty, cd.description,"; $sql .= " p.ref as product_ref, p.label as product_label"; $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product"; $sql .= " WHERE cd.rowid = ".((int)$line_id); $resql = $db->query($sql); if ($resql && ($obj = $db->fetch_object($resql))) { // Prüfe ob schon auf diesem Stundenzettel $sql2 = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel_product"; $sql2 .= " WHERE fk_stundenzettel = ".((int)$use_stundenzettel_id); $sql2 .= " AND fk_commandedet = ".((int)$line_id); $resql2 = $db->query($sql2); if ($resql2 && $db->num_rows($resql2) == 0) { // Noch nicht vorhanden, hinzufügen $stundenzettel->addProduct( $obj->fk_product, $obj->rowid, null, $obj->qty, 0, 'order', $obj->description // Beschreibung für Freitext-Produkte ); } } } // Mehraufwand-Produkte in Produktliste übernehmen (falls ausgewählt) // Erstellt eine neue Produktzeile (origin='added') im Ziel-Stundenzettel if (!empty($selected_mehraufwand)) { foreach ($selected_mehraufwand as $ma_ids) { // $ma_ids kann mehrere IDs enthalten (kommasepariert) $ids = explode(',', $ma_ids); foreach ($ids as $sp_id) { $sp_id = (int)$sp_id; if ($sp_id <= 0) continue; // Produktinfo aus der Mehraufwand-Zeile holen $sqlMa = "SELECT sp.fk_product, sp.product_ref, sp.product_label, sp.description, sp.qty_done"; $sqlMa .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; $sqlMa .= " WHERE sp.rowid = ".((int)$sp_id); $resqlMa = $db->query($sqlMa); if ($resqlMa && ($objMa = $db->fetch_object($resqlMa))) { $qty = max(1, (float)$objMa->qty_done); // Mindestens 1 // Prüfe ob Produkt schon auf diesem Stundenzettel (in Produktliste) existiert $sqlCheck = "SELECT rowid, qty_done FROM ".MAIN_DB_PREFIX."stundenzettel_product"; $sqlCheck .= " WHERE fk_stundenzettel = ".((int)$use_stundenzettel_id); $sqlCheck .= " AND origin = 'added'"; if ($objMa->fk_product > 0) { $sqlCheck .= " AND fk_product = ".((int)$objMa->fk_product); } else { $sqlCheck .= " AND fk_product IS NULL AND description = '".$db->escape($objMa->description)."'"; } $resqlCheck = $db->query($sqlCheck); if ($resqlCheck && $db->num_rows($resqlCheck) > 0) { // Existiert bereits - Menge erhöhen $objExist = $db->fetch_object($resqlCheck); $newQty = (float)$objExist->qty_done + $qty; $sqlUpd = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_product"; $sqlUpd .= " SET qty_done = ".$newQty; $sqlUpd .= " WHERE rowid = ".((int)$objExist->rowid); $db->query($sqlUpd); } else { // Neu als Produktzeile anlegen (origin='added') $stundenzettel->addProduct( $objMa->fk_product, null, // kein fk_commandedet (nicht im Auftrag) null, // qty_original 0, // qty_ordered $qty, // qty_done 'added', // origin = in Produktliste hinzugefügt $objMa->description ); } } } } } setEventMessages($langs->trans('ProductsTransferred'), null, 'mesgs'); header('Location: '.dol_buildpath('/stundenzettel/card.php?id='.$use_stundenzettel_id.'&tab=products', 1)); exit; } else { setEventMessages($langs->trans('ErrorCreatingStundenzettel'), null, 'errors'); } } } // Notiz abhaken (von stundenzettel_commande aus) if ($action == 'toggle_note' && $user->hasRight('stundenzettel', 'write')) { $note_id = GETPOST('note_id', 'int'); $checked = GETPOST('checked', 'int'); $stz_id = GETPOST('stundenzettel_id', 'int'); if ($stz_id > 0) { $stzObj = new Stundenzettel($db); if ($stzObj->fetch($stz_id) > 0) { $result = $stzObj->updateNoteStatus($note_id, $checked); if ($result > 0) { setEventMessages($langs->trans('RecordModified'), null, 'mesgs'); } } } header('Location: '.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1'.($stundenzettel_id > 0 ? '&stundenzettel_id='.$stundenzettel_id : '')); exit; } // Stundenzettel für Auftrag freigeben (direkt oder nach Bestätigung) // Berechtigung: write ist ausreichend (validate ist optional) $canRelease = $user->hasRight('stundenzettel', 'write') || $user->hasRight('stundenzettel', 'validate') || $user->admin; $doRelease = false; if ($action == 'release_stundenzettel' && $canRelease) { $doRelease = true; } if ($action == 'release_stundenzettel_confirmed' && GETPOST('confirm', 'alpha') == 'yes' && $canRelease) { $doRelease = true; } // Auch confirm_release akzeptieren (Fallback) if ($action == 'confirm_release' && GETPOST('confirm', 'alpha') == 'yes' && $canRelease) { $doRelease = true; } if ($doRelease) { // Direkt in Datenbank updaten (zuverlässiger als insertExtraFields) $sql = "UPDATE ".MAIN_DB_PREFIX."commande_extrafields SET stundenzettel_status = 1 WHERE fk_object = ".((int)$order->id); $resql = $db->query($sql); if ($resql) { // Falls noch kein Eintrag existiert, einen erstellen if ($db->affected_rows($resql) == 0) { $sql2 = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_status) VALUES (".((int)$order->id).", 1)"; $db->query($sql2); } setEventMessages($langs->trans('StundenzettelReleased'), null, 'mesgs'); } else { setEventMessages($db->lasterror(), null, 'errors'); } header('Location: '.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1&filter='.$filter); exit; } // Stundenzettel für Auftrag wiedereröffnen $canReopen = $user->hasRight('stundenzettel', 'write') || $user->hasRight('stundenzettel', 'validate') || $user->admin; if ($action == 'reopen_stundenzettel' && $canReopen) { // Direkt in Datenbank updaten (zuverlässiger als insertExtraFields) $sql = "UPDATE ".MAIN_DB_PREFIX."commande_extrafields SET stundenzettel_status = 0 WHERE fk_object = ".((int)$order->id); $resql = $db->query($sql); if ($resql) { // Falls noch kein Eintrag existiert, einen erstellen if ($db->affected_rows($resql) == 0) { $sql2 = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_status) VALUES (".((int)$order->id).", 0)"; $db->query($sql2); } setEventMessages($langs->trans('StundenzettelReopened'), null, 'mesgs'); } else { setEventMessages($db->lasterror(), null, 'errors'); } header('Location: '.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1&filter='.$filter); exit; } // Stundenzettel komplett zurücksetzen (von Status 2 auf 0) $canReset = $user->hasRight('stundenzettel', 'write') || $user->admin; if ($action == 'reset_stundenzettel' && $canReset) { // Direkt in Datenbank updaten (zuverlässiger als insertExtraFields) $sql = "UPDATE ".MAIN_DB_PREFIX."commande_extrafields SET stundenzettel_status = 0 WHERE fk_object = ".((int)$order->id); $resql = $db->query($sql); if ($resql) { // Falls noch kein Eintrag existiert, einen erstellen if ($db->affected_rows($resql) == 0) { $sql2 = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_status) VALUES (".((int)$order->id).", 0)"; $db->query($sql2); } setEventMessages($langs->trans('StundenzettelReset'), null, 'mesgs'); } else { setEventMessages($db->lasterror(), null, 'errors'); } header('Location: '.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1&filter='.$filter); exit; } // In Rechnung übernehmen if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes' && $user->hasRight('facture', 'creer')) { require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php'; $db->begin(); $error = 0; // Neue Rechnung erstellen $facture = new Facture($db); $facture->socid = $order->socid; $facture->type = Facture::TYPE_STANDARD; $facture->date = dol_now(); $facture->fk_project = $order->fk_project; $facture->cond_reglement_id = $order->cond_reglement_id; $facture->mode_reglement_id = $order->mode_reglement_id; $facture->note_private = $langs->trans('CreatedFromStundenzettel').' - '.$order->ref; $facture->linked_objects['commande'] = $order->id; $facture_id = $facture->create($user); if ($facture_id > 0) { // Sammle alle Produkt-Mengen aus Stundenzetteln (gruppiert nach fk_commandedet) $productQtys = array(); $sqlQty = "SELECT sp.fk_commandedet, SUM(sp.qty_done) as total_qty"; $sqlQty .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; $sqlQty .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; $sqlQty .= " WHERE s.fk_commande = ".((int)$order->id); $sqlQty .= " AND sp.origin IN ('order', 'added')"; $sqlQty .= " AND sp.fk_commandedet IS NOT NULL"; $sqlQty .= " GROUP BY sp.fk_commandedet"; $resqlQty = $db->query($sqlQty); if ($resqlQty) { while ($objQty = $db->fetch_object($resqlQty)) { $productQtys[$objQty->fk_commandedet] = floatval($objQty->total_qty); } } // Sammle Mehraufwand (zusätzliche Produkte mit origin = 'additional') $additionalProducts = array(); $sqlAdd = "SELECT sp.fk_product, sp.product_label, sp.description, SUM(sp.qty_done) as total_qty"; $sqlAdd .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; $sqlAdd .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; $sqlAdd .= " WHERE s.fk_commande = ".((int)$order->id); $sqlAdd .= " AND sp.origin = 'additional'"; $sqlAdd .= " GROUP BY sp.fk_product, sp.product_label, sp.description"; $resqlAdd = $db->query($sqlAdd); if ($resqlAdd) { while ($objAdd = $db->fetch_object($resqlAdd)) { if (floatval($objAdd->total_qty) > 0) { $additionalProducts[] = $objAdd; } } } // Lade Section-Hierarchie aus llx_facture_lines_manager $sqlManager = "SELECT m.rowid, m.line_type, m.fk_commandedet, m.title, m.parent_section, m.line_order, m.show_subtotal,"; $sqlManager .= " cd.fk_product, cd.qty as qty_ordered, cd.subprice, cd.tva_tx, cd.remise_percent,"; $sqlManager .= " cd.description, cd.product_type, cd.special_code, cd.fk_unit,"; $sqlManager .= " p.ref as product_ref, p.label as product_label"; $sqlManager .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m"; $sqlManager .= " LEFT JOIN ".MAIN_DB_PREFIX."commandedet cd ON cd.rowid = m.fk_commandedet"; $sqlManager .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = cd.fk_product"; $sqlManager .= " WHERE m.fk_commande = ".((int)$order->id); $sqlManager .= " AND m.document_type = 'order'"; $sqlManager .= " ORDER BY m.line_order"; $resqlManager = $db->query($sqlManager); if ($resqlManager) { // Sammle alle Zeilen nach Parent-Section gruppiert $sections = array(); // section_id => section data $sectionProducts = array(); // section_id => array of products $orphanProducts = array(); // Products without section while ($obj = $db->fetch_object($resqlManager)) { if ($obj->line_type == 'section') { $sections[$obj->rowid] = $obj; if (!isset($sectionProducts[$obj->rowid])) { $sectionProducts[$obj->rowid] = array(); } } elseif ($obj->line_type == 'product') { $parentId = $obj->parent_section ? $obj->parent_section : 0; if ($parentId > 0) { if (!isset($sectionProducts[$parentId])) { $sectionProducts[$parentId] = array(); } $sectionProducts[$parentId][] = $obj; } else { $orphanProducts[] = $obj; } } // subtotal wird automatisch nach Section hinzugefügt wenn show_subtotal=1 } $rang = 0; $invoiceManagerLines = array(); // Für llx_facture_lines_manager der Rechnung // Verarbeite jede Section foreach ($sections as $sectionId => $section) { $products = isset($sectionProducts[$sectionId]) ? $sectionProducts[$sectionId] : array(); $sectionHasInvoicedProducts = false; $sectionSubtotal = 0; // Prüfe ob Section Produkte mit qty > 0 hat foreach ($products as $prod) { $qtyToInvoice = isset($productQtys[$prod->fk_commandedet]) ? $productQtys[$prod->fk_commandedet] : 0; if ($qtyToInvoice > 0) { $sectionHasInvoicedProducts = true; break; } } // Nur Sections mit tatsächlich verwendeten Produkten hinzufügen if ($sectionHasInvoicedProducts) { // Section-Titel hinzufügen (special_code = 100) $result = $facture->addline( $section->title, 0, // subprice 0, // qty 0, // tva_tx 0, 0, // localtax 0, // fk_product 0, // remise_percent '', '', // dates 0, 0, '', 'HT', 0, 9, // product_type (9 = Title) $rang++, 100, // special_code = 100 für Section 0, '', 0, 0 ); if ($result > 0) { $invoiceManagerLines[] = array( 'type' => 'section', 'fk_facturedet' => $result, 'title' => $section->title, 'parent' => null ); $currentSectionDetId = $result; } // Produkte der Section hinzufügen foreach ($products as $prod) { $qtyToInvoice = isset($productQtys[$prod->fk_commandedet]) ? $productQtys[$prod->fk_commandedet] : 0; if ($qtyToInvoice > 0) { $result = $facture->addline( $prod->description, // 1: desc $prod->subprice, // 2: pu_ht $qtyToInvoice, // 3: qty $prod->tva_tx, // 4: txtva 0, // 5: txlocaltax1 0, // 6: txlocaltax2 $prod->fk_product, // 7: fk_product $prod->remise_percent, // 8: remise_percent '', // 9: date_start '', // 10: date_end 0, // 11: fk_code_ventilation 0, // 12: info_bits 0, // 13: fk_remise_except 'HT', // 14: price_base_type 0, // 15: pu_ttc $prod->product_type, // 16: type $rang++, // 17: rang 0, // 18: special_code '', // 19: origin 0, // 20: origin_id 0, // 21: fk_parent_line null, // 22: fk_fournprice 0, // 23: pa_ht '', // 24: label array(), // 25: array_options 100, // 26: situation_percent 0, // 27: fk_prev_id $prod->fk_unit // 28: fk_unit ); if ($result > 0) { $invoiceManagerLines[] = array( 'type' => 'product', 'fk_facturedet' => $result, 'title' => null, 'parent' => $currentSectionDetId ); $sectionSubtotal += $prod->subprice * $qtyToInvoice; } elseif ($result < 0) { $error++; setEventMessages($facture->error, $facture->errors, 'errors'); } } } // Zwischensumme hinzufügen wenn Section show_subtotal hat if ($section->show_subtotal && $sectionSubtotal > 0) { $subtotalLabel = 'Zwischensumme: '.$section->title; $result = $facture->addline( $subtotalLabel, 0, // wird automatisch berechnet 1, // qty 0, // tva_tx 0, 0, // localtax 0, // fk_product 0, // remise_percent '', '', // dates 0, 0, '', 'HT', 0, 9, // product_type $rang++, 102, // special_code = 102 für Subtotal 0, '', 0, 0 ); if ($result > 0) { $invoiceManagerLines[] = array( 'type' => 'subtotal', 'fk_facturedet' => $result, 'title' => $subtotalLabel, 'parent' => $currentSectionDetId ); } } } } // Prüfe ob im Auftrag überhaupt Sections vorhanden sind $orderHasSections = (count($sections) > 0); // Orphan-Produkte (ohne Section) hinzufügen // Nur als eigene Section "Sonstige Produkte" wenn im Auftrag Sections vorhanden sind $hasOrphansWithQty = false; foreach ($orphanProducts as $prod) { $qtyToInvoice = isset($productQtys[$prod->fk_commandedet]) ? $productQtys[$prod->fk_commandedet] : 0; if ($qtyToInvoice > 0) { $hasOrphansWithQty = true; break; } } if ($hasOrphansWithQty) { $sonstigeSectionResult = 0; // Nur Section erstellen wenn im Auftrag auch Sections vorhanden sind if ($orderHasSections) { $sonstigeSectionResult = $facture->addline( $langs->trans('OtherProducts'), 0, 0, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0, 9, // product_type für Titel $rang++, 100 // special_code = 100 für Section ); if ($sonstigeSectionResult > 0) { $invoiceManagerLines[] = array( 'type' => 'section', 'fk_facturedet' => $sonstigeSectionResult, 'title' => $langs->trans('OtherProducts'), 'parent' => null ); } } foreach ($orphanProducts as $prod) { $qtyToInvoice = isset($productQtys[$prod->fk_commandedet]) ? $productQtys[$prod->fk_commandedet] : 0; if ($qtyToInvoice > 0) { $result = $facture->addline( $prod->description, // 1: desc $prod->subprice, // 2: pu_ht $qtyToInvoice, // 3: qty $prod->tva_tx, // 4: txtva 0, // 5: txlocaltax1 0, // 6: txlocaltax2 $prod->fk_product, // 7: fk_product $prod->remise_percent, // 8: remise_percent '', // 9: date_start '', // 10: date_end 0, // 11: fk_code_ventilation 0, // 12: info_bits 0, // 13: fk_remise_except 'HT', // 14: price_base_type 0, // 15: pu_ttc $prod->product_type, // 16: type $rang++, // 17: rang 0, // 18: special_code '', // 19: origin 0, // 20: origin_id 0, // 21: fk_parent_line null, // 22: fk_fournprice 0, // 23: pa_ht '', // 24: label array(), // 25: array_options 100, // 26: situation_percent 0, // 27: fk_prev_id $prod->fk_unit // 28: fk_unit ); if ($result > 0 && $orderHasSections && $sonstigeSectionResult > 0) { $invoiceManagerLines[] = array( 'type' => 'product', 'fk_facturedet' => $result, 'title' => null, 'parent' => $sonstigeSectionResult ); } elseif ($result < 0) { $error++; setEventMessages($facture->error, $facture->errors, 'errors'); } } } // Zwischensumme für Sonstige Produkte if ($sonstigeSectionResult > 0) { $subtotalLabel = 'Zwischensumme: '.$langs->trans('OtherProducts'); $subtotalResult = $facture->addline( $subtotalLabel, 0, 1, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0, 9, $rang++, 102, 0, '', 0, 0 ); if ($subtotalResult > 0) { $invoiceManagerLines[] = array( 'type' => 'subtotal', 'fk_facturedet' => $subtotalResult, 'title' => $subtotalLabel, 'parent' => $sonstigeSectionResult ); } } } // Mehraufwand hinzufügen (am Ende) // Nur als Section wenn im Auftrag auch Sections vorhanden sind if (count($additionalProducts) > 0) { $mehraufwandSectionResult = 0; // Nur Section erstellen wenn im Auftrag auch Sections vorhanden sind if ($orderHasSections) { $mehraufwandSectionResult = $facture->addline( $langs->trans('Mehraufwand'), 0, 0, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0, 9, // product_type für Titel $rang++, 100 // special_code = 100 für Section ); if ($mehraufwandSectionResult > 0) { $invoiceManagerLines[] = array( 'type' => 'section', 'fk_facturedet' => $mehraufwandSectionResult, 'title' => $langs->trans('Mehraufwand'), 'parent' => null ); } } foreach ($additionalProducts as $addProd) { $productResult = 0; if ($addProd->fk_product > 0) { // Katalog-Produkt $product = new Product($db); $product->fetch($addProd->fk_product); // Kundenspezifischen Preis holen (falls vorhanden) $customerPriceInfo = getCustomerPrice($db, $addProd->fk_product, $order->socid, $product); $usePrice = $customerPriceInfo['price']; $useTvaTx = $customerPriceInfo['tva_tx']; $productResult = $facture->addline( $product->label, // 1: desc $usePrice, // 2: pu_ht - kundenspezifischer Preis $addProd->total_qty, // 3: qty $useTvaTx, // 4: txtva - kundenspezifischer MwSt-Satz 0, // 5: txlocaltax1 0, // 6: txlocaltax2 $addProd->fk_product, // 7: fk_product 0, // 8: remise_percent '', // 9: date_start '', // 10: date_end 0, // 11: fk_code_ventilation 0, // 12: info_bits 0, // 13: fk_remise_except 'HT', // 14: price_base_type 0, // 15: pu_ttc $product->type, // 16: type $rang++, // 17: rang 0, // 18: special_code '', // 19: origin 0, // 20: origin_id 0, // 21: fk_parent_line null, // 22: fk_fournprice 0, // 23: pa_ht '', // 24: label array(), // 25: array_options 100, // 26: situation_percent 0, // 27: fk_prev_id $product->fk_unit // 28: fk_unit ); } else { // Freitext $productResult = $facture->addline( $addProd->description ? $addProd->description : $addProd->product_label, 0, // 2: pu_ht $addProd->total_qty, // 3: qty 0, // 4: txtva 0, // 5: txlocaltax1 0, // 6: txlocaltax2 0, // 7: fk_product 0, // 8: remise_percent '', // 9: date_start '', // 10: date_end 0, // 11: fk_code_ventilation 0, // 12: info_bits 0, // 13: fk_remise_except 'HT', // 14: price_base_type 0, // 15: pu_ttc 0, // 16: type $rang++, // 17: rang 0, // 18: special_code '', // 19: origin 0, // 20: origin_id 0, // 21: fk_parent_line null, // 22: fk_fournprice 0, // 23: pa_ht '', // 24: label array(), // 25: array_options 100, // 26: situation_percent 0, // 27: fk_prev_id 0 // 28: fk_unit ); } if ($productResult > 0 && $orderHasSections && $mehraufwandSectionResult > 0) { $invoiceManagerLines[] = array( 'type' => 'product', 'fk_facturedet' => $productResult, 'title' => null, 'parent' => $mehraufwandSectionResult ); } elseif ($productResult < 0) { $error++; setEventMessages($facture->error, $facture->errors, 'errors'); } } // Zwischensumme für Mehraufwand if ($mehraufwandSectionResult > 0) { $subtotalLabel = 'Zwischensumme: '.$langs->trans('Mehraufwand'); $subtotalResult = $facture->addline( $subtotalLabel, 0, 1, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0, 9, $rang++, 102, 0, '', 0, 0 ); if ($subtotalResult > 0) { $invoiceManagerLines[] = array( 'type' => 'subtotal', 'fk_facturedet' => $subtotalResult, 'title' => $subtotalLabel, 'parent' => $mehraufwandSectionResult ); } } } // ============================================ // ARBEITSZEITEN aus Stundenzetteln hinzufügen // (Gruppiert nach Leistungsposition/Produkt) // ============================================ // Standard-Leistung vom Kunden laden (Fallback wenn keine Leistung gewählt) $societe = new Societe($db); $societe->fetch($order->socid); $societe->fetch_optionals(); $defaultServiceId = isset($societe->array_options['options_stundenzettel_default_service']) ? (int)$societe->array_options['options_stundenzettel_default_service'] : 0; // Alle Arbeitszeiten nach Leistungsposition gruppiert sammeln $sqlHours = "SELECT "; $sqlHours .= " COALESCE(l.fk_product, ".(int)$defaultServiceId.") as product_id,"; $sqlHours .= " p.ref as product_ref, p.label as product_label, p.tva_tx, p.fk_unit,"; $sqlHours .= " SUM(l.duration) as total_minutes"; $sqlHours .= " FROM ".MAIN_DB_PREFIX."stundenzettel s"; $sqlHours .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel_leistung l ON l.fk_stundenzettel = s.rowid"; $sqlHours .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = COALESCE(l.fk_product, ".(int)$defaultServiceId.")"; $sqlHours .= " WHERE s.fk_commande = ".((int)$order->id); $sqlHours .= " AND s.status >= 1"; // Nur validierte Stundenzettel $sqlHours .= " GROUP BY COALESCE(l.fk_product, ".(int)$defaultServiceId."), p.ref, p.label, p.tva_tx, p.fk_unit"; $sqlHours .= " HAVING SUM(l.duration) > 0"; $sqlHours .= " ORDER BY p.label"; $resqlHours = $db->query($sqlHours); $hasWorkHours = ($resqlHours && $db->num_rows($resqlHours) > 0); if ($hasWorkHours) { // Arbeitszeit-Section erstellen (nur wenn Sections im Auftrag) $arbeitszeitSectionResult = 0; if ($orderHasSections) { $arbeitszeitSectionResult = $facture->addline( $langs->trans('Leistungen'), 0, 0, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0, 9, // product_type für Titel $rang++, 100 // special_code = 100 für Section ); if ($arbeitszeitSectionResult > 0) { $invoiceManagerLines[] = array( 'type' => 'section', 'fk_facturedet' => $arbeitszeitSectionResult, 'title' => $langs->trans('Leistungen'), 'parent' => null ); } } // Arbeitszeiten hinzufügen (pro Leistungsposition) $arbeitszeitSubtotal = 0; while ($objHours = $db->fetch_object($resqlHours)) { $productId = (int)$objHours->product_id; $hoursWorked = $objHours->total_minutes / 60; // Produkt-Preis ermitteln (kundenspezifisch oder Standard) $priceInfo = getCustomerPrice($db, $productId, $order->socid); $hourlyPrice = $priceInfo['price']; $tvaTx = !empty($objHours->tva_tx) ? $objHours->tva_tx : $priceInfo['tva_tx']; // Beschreibung $lineDesc = !empty($objHours->product_label) ? $objHours->product_label : $langs->trans('DefaultService'); // Rechnungszeile hinzufügen $hoursResult = $facture->addline( $lineDesc, // 1: desc $hourlyPrice, // 2: pu_ht $hoursWorked, // 3: qty (Stunden) $tvaTx, // 4: txtva 0, // 5: txlocaltax1 0, // 6: txlocaltax2 $productId, // 7: fk_product 0, // 8: remise_percent '', // 9: date_start '', // 10: date_end 0, // 11: fk_code_ventilation 0, // 12: info_bits 0, // 13: fk_remise_except 'HT', // 14: price_base_type 0, // 15: pu_ttc 1, // 16: type (1 = Dienstleistung) $rang++, // 17: rang 0, // 18: special_code '', // 19: origin 0, // 20: origin_id 0, // 21: fk_parent_line null, // 22: fk_fournprice 0, // 23: pa_ht '', // 24: label array(), // 25: array_options 100, // 26: situation_percent 0, // 27: fk_prev_id $objHours->fk_unit // 28: fk_unit ); if ($hoursResult > 0) { if ($orderHasSections && $arbeitszeitSectionResult > 0) { $invoiceManagerLines[] = array( 'type' => 'product', 'fk_facturedet' => $hoursResult, 'title' => null, 'parent' => $arbeitszeitSectionResult ); } $arbeitszeitSubtotal += $hourlyPrice * $hoursWorked; } elseif ($hoursResult < 0) { $error++; setEventMessages($facture->error, $facture->errors, 'errors'); } } // Zwischensumme für Arbeitszeit if ($arbeitszeitSectionResult > 0 && $arbeitszeitSubtotal > 0) { $subtotalLabel = 'Zwischensumme: '.$langs->trans('Leistungen'); $subtotalResult = $facture->addline( $subtotalLabel, 0, 1, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0, 9, $rang++, 102, 0, '', 0, 0 ); if ($subtotalResult > 0) { $invoiceManagerLines[] = array( 'type' => 'subtotal', 'fk_facturedet' => $subtotalResult, 'title' => $subtotalLabel, 'parent' => $arbeitszeitSectionResult ); } } } // llx_facture_lines_manager für Rechnung erstellen if (!$error && count($invoiceManagerLines) > 0) { $lineOrder = 1; $sectionMap = array(); // old_facturedet_id => new_manager_rowid foreach ($invoiceManagerLines as $mLine) { $sqlInsert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager"; $sqlInsert .= " (fk_facture, document_type, line_type, fk_facturedet, title, parent_section, line_order, show_subtotal, date_creation)"; $sqlInsert .= " VALUES ("; $sqlInsert .= ((int)$facture_id).", 'invoice', "; $sqlInsert .= "'".$db->escape($mLine['type'])."', "; $sqlInsert .= ($mLine['fk_facturedet'] ? (int)$mLine['fk_facturedet'] : "NULL").", "; $sqlInsert .= ($mLine['title'] ? "'".$db->escape($mLine['title'])."'" : "NULL").", "; // Parent-Section mapping if ($mLine['parent'] && isset($sectionMap[$mLine['parent']])) { $sqlInsert .= (int)$sectionMap[$mLine['parent']].", "; } else { $sqlInsert .= "NULL, "; } $sqlInsert .= $lineOrder++.", "; $sqlInsert .= ($mLine['type'] == 'section' ? "1" : "0").", "; $sqlInsert .= "NOW())"; $db->query($sqlInsert); // Track section IDs for parent mapping if ($mLine['type'] == 'section') { $sectionMap[$mLine['fk_facturedet']] = $db->last_insert_id(MAIN_DB_PREFIX.'facture_lines_manager'); } } } } else { $error++; } if (!$error) { // ============================================ // Rang-Werte in facturedet korrigieren // Synchronisiert rang mit line_order aus facture_lines_manager // ============================================ $sqlFixRang = "UPDATE ".MAIN_DB_PREFIX."facturedet fd INNER JOIN ".MAIN_DB_PREFIX."facture_lines_manager flm ON flm.fk_facturedet = fd.rowid SET fd.rang = flm.line_order WHERE flm.fk_facture = ".((int)$facture_id); $db->query($sqlFixRang); // Status auf "in Rechnung übertragen" setzen (direkt per SQL) $sqlUpdate = "UPDATE ".MAIN_DB_PREFIX."commande_extrafields SET stundenzettel_status = 2 WHERE fk_object = ".((int)$order->id); $resqlUpdate = $db->query($sqlUpdate); if (!$resqlUpdate || $db->affected_rows($resqlUpdate) == 0) { $sqlInsert = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_status) VALUES (".((int)$order->id).", 2)"; $db->query($sqlInsert); } $db->commit(); setEventMessages($langs->trans('InvoiceCreated'), null, 'mesgs'); header('Location: '.DOL_URL_ROOT.'/compta/facture/card.php?id='.$facture_id); exit; } else { $db->rollback(); } } else { $error++; setEventMessages($facture->error, $facture->errors, 'errors'); $db->rollback(); } header('Location: '.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1'); exit; } /* * View */ // Linkes Menü aktivieren $_GET['mainmenu'] = 'stundenzettel'; // Stundenzettel laden wenn ID übergeben wurde $stundenzettelObj = null; if ($stundenzettel_id > 0) { $stundenzettelObj = new Stundenzettel($db); if ($stundenzettelObj->fetch($stundenzettel_id) <= 0) { $stundenzettelObj = null; $stundenzettel_id = 0; } } // Lade Extrafields für Stundenzettel-Status - direkt aus DB lesen für Zuverlässigkeit $sqlStatus = "SELECT stundenzettel_status FROM ".MAIN_DB_PREFIX."commande_extrafields WHERE fk_object = ".((int)$order->id); $resqlStatus = $db->query($sqlStatus); $stundenzettelStatus = 0; if ($resqlStatus && ($objStatus = $db->fetch_object($resqlStatus))) { $stundenzettelStatus = (int)$objStatus->stundenzettel_status; } // 0 = Offen, 1 = Freigegeben, 2 = In Rechnung übertragen // Prüfe ob alle Stundenzettel validiert sind $allValidated = false; $hasStundenzettel = false; $sqlCheck = "SELECT COUNT(*) as total, SUM(CASE WHEN status >= 1 THEN 1 ELSE 0 END) as validated"; $sqlCheck .= " FROM ".MAIN_DB_PREFIX."stundenzettel WHERE fk_commande = ".((int)$order->id); $resqlCheck = $db->query($sqlCheck); if ($resqlCheck) { $objCheck = $db->fetch_object($resqlCheck); $hasStundenzettel = ($objCheck->total > 0); $allValidated = ($objCheck->total > 0 && $objCheck->total == $objCheck->validated); } // Berechne verbleibende Produkte $remainingProducts = array(); if ($hasStundenzettel) { $sqlRemaining = "SELECT cd.rowid, cd.fk_product, cd.qty as qty_ordered, cd.description,"; $sqlRemaining .= " p.ref as product_ref, p.label as product_label,"; $sqlRemaining .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; $sqlRemaining .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; $sqlRemaining .= " WHERE sp.fk_commandedet = cd.rowid AND sp.origin IN ('order', 'added', 'omitted')), 0) as qty_documented"; $sqlRemaining .= " FROM ".MAIN_DB_PREFIX."commandedet as cd"; $sqlRemaining .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product"; $sqlRemaining .= " WHERE cd.fk_commande = ".((int)$order->id); $sqlRemaining .= " AND (cd.fk_product > 0 OR (cd.fk_product = 0 AND cd.description IS NOT NULL AND cd.description != ''))"; $sqlRemaining .= " AND (cd.special_code IS NULL OR cd.special_code = 0)"; $sqlRemaining .= " HAVING qty_ordered > qty_documented"; $resqlRemaining = $db->query($sqlRemaining); if ($resqlRemaining) { while ($objRem = $db->fetch_object($resqlRemaining)) { $remainingProducts[] = $objRem; } } } $hasRemainingProducts = (count($remainingProducts) > 0); $title = $langs->trans("Stundenzettel").' - '.$order->ref; llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-stundenzettel page-commande'); // Mobile CSS einbinden $mobileCssFile = dol_buildpath('/stundenzettel/css/stundenzettel-mobile.css', 0); if (file_exists($mobileCssFile)) { print ''; } // Tabs - Immer die Stundenzettel-Commande-Tabs verwenden // Aktiven Tab-Key basierend auf $tab bestimmen $activeTabKey = $tab; // products, stundenzettel, oder tracking $head = stundenzettel_commande_prepare_head($order, $stundenzettel_id); dol_fiche_head($head, $activeTabKey, $langs->trans("Stundenzettel"), -1, 'clock'); $form = new Form($db); // Bestätigung für Freigabe mit Warnung (verbleibende Produkte) if ($action == 'confirm_release_warning') { // Zeige Warnung mit verbleibenden Produkten if ($hasRemainingProducts && count($remainingProducts) > 0) { // Erstelle HTML für die Produktliste $remainingHtml = '
'; $remainingHtml .= ''.$langs->trans("RemainingProductsWarning").':
'; $confirmMessage = $langs->trans('ConfirmReleaseWithRemaining').$remainingHtml.'

'.$langs->trans('ConfirmReleaseFinal').''; } else { // Keine verbleibenden Produkte - einfache Bestätigung $confirmMessage = $langs->trans('ConfirmReleaseFinal'); } // Direkt zur Freigabe-Action - nur EIN Dialog print $form->formconfirm( $_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1', $langs->trans('ConfirmReleaseWithRemainingTitle'), $confirmMessage, 'confirm_release', // Einfacher Action-Name array(), 0, 0 // useajax=0 für zuverlässigere Darstellung ); } // Bestätigung für Rechnungsübertragung if ($action == 'transfer_invoice') { print $form->formconfirm( $_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1', $langs->trans('ConfirmTransferInvoiceTitle'), $langs->trans('ConfirmTransferInvoice'), 'confirm_transfer_invoice', array(), 0, 1 ); } // Info banner - Produktliste zeigt immer den Auftrag, andere Tabs den Stundenzettel if ($tab == 'products') { // Produktliste ist tagesbasiert - immer Auftrag anzeigen $linkback = ''.$langs->trans("BackToList").''; dol_banner_tab($order, 'ref', $linkback, 1, 'ref', 'ref'); } elseif ($stundenzettelObj) { $linkback = ''.$langs->trans("BackToList").''; dol_banner_tab($stundenzettelObj, 'id', $linkback, 1, 'rowid', 'ref'); } else { $linkback = ''.$langs->trans("BackToList").''; dol_banner_tab($order, 'ref', $linkback, 1, 'ref', 'ref'); } print '
'; print '
'; // Standard-Leistung und geplante Stunden laden $societe = new Societe($db); $societe->fetch($order->socid); $societe->fetch_optionals(); // Standard-Leistung vom Kunden $defaultServiceId = isset($societe->array_options['options_stundenzettel_default_service']) ? (int)$societe->array_options['options_stundenzettel_default_service'] : 0; $defaultServiceProduct = null; if ($defaultServiceId > 0) { $defaultServiceProduct = new Product($db); if ($defaultServiceProduct->fetch($defaultServiceId) <= 0) { $defaultServiceProduct = null; } } // Geplante Stunden aus dem Auftrag suchen (Dienstleistungen = fk_product_type = 1) $plannedHours = 0; $plannedHoursLine = null; foreach ($order->lines as $line) { // Prüfe ob es eine Dienstleistung ist if ($line->fk_product > 0) { $prod = new Product($db); if ($prod->fetch($line->fk_product) > 0 && $prod->type == 1) { // Es ist eine Dienstleistung - summiere die Menge $plannedHours += $line->qty; if (!$plannedHoursLine) { $plannedHoursLine = $line; // Erste Dienstleistung merken } } } } // Info-Box für Standard-Leistung und geplante Stunden if ($defaultServiceProduct || $plannedHours > 0) { print '
'; print ''; // Standard-Leistung print ''; print ''; print ''; print ''; // Geplante Stunden if ($plannedHours > 0) { print ''; print ''; print ''; print ''; } print '
'.img_picto('', 'service', 'class="pictofixedwidth"').$langs->trans("DefaultService").':'; if ($defaultServiceProduct) { // Kundenspezifischen Preis für die Standard-Leistung holen $defaultServicePriceInfo = getCustomerPrice($db, $defaultServiceProduct->id, $order->socid, $defaultServiceProduct); $displayPrice = $defaultServicePriceInfo['price']; $isCustomerPrice = $defaultServicePriceInfo['is_customer_price']; print $defaultServiceProduct->getNomUrl(1).' - '.$defaultServiceProduct->label; print ' ('.price($displayPrice, 0, $langs, 1, -1, -1, $conf->currency).'/Std.)'; if ($isCustomerPrice) { print ' Kundenpreis'; } print ''; } else { print ''.$langs->trans("NoDefaultServiceSet").''; print ' '.$langs->trans("SetDefaultServiceInCustomer").''; } print '
'.img_picto('', 'clock', 'class="pictofixedwidth"').$langs->trans("PlannedHours").':'.formatQty($plannedHours).' Std. '.$langs->trans("HoursFromOrder").'
'; print '
'; } // Auftragsbeschreibung anzeigen (Extrafeld auftragsbeschreibung) // Extrafelder laden falls noch nicht geladen if (!isset($order->array_options) || empty($order->array_options)) { $order->fetch_optionals(); } $orderDescription = isset($order->array_options['options_auftragsbeschreibung']) ? $order->array_options['options_auftragsbeschreibung'] : ''; if (!empty($orderDescription)) { print '
'; print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print '
'.img_picto('', 'note', 'class="pictofixedwidth"').$langs->trans("OrderDescription").'
'.dol_htmlentitiesbr($orderDescription).'
'; print '
'; print '
'; print '
'; } // Notizen für nächsten Besuch (aus vorherigen Stundenzetteln) $sql = "SELECT s.ref, s.note_public, s.date_stundenzettel"; $sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel as s"; $sql .= " WHERE s.fk_commande = ".((int)$order->id); $sql .= " AND s.note_public IS NOT NULL AND s.note_public != ''"; $sql .= " ORDER BY s.date_stundenzettel DESC"; $sql .= " LIMIT 3"; $resql = $db->query($sql); if ($resql && $db->num_rows($resql) > 0) { print '
'; print ''.img_picto('', 'note', 'class="pictofixedwidth"').$langs->trans("NotesForNextVisit").':
'; while ($obj = $db->fetch_object($resql)) { print '
'; print ''.dol_print_date($db->jdate($obj->date_stundenzettel), 'day').' ('.$obj->ref.'):
'; print dol_htmlentitiesbr($obj->note_public); print '
'; } print '
'; } // Merkzettel/Notizen aus verknüpftem Stundenzettel anzeigen $notesStundenzettel = null; $notesToShow = array(); // Stundenzettel für Notizen bestimmen if ($stundenzettelObj) { $notesStundenzettel = $stundenzettelObj; } else { // Offenen Stundenzettel für diesen Auftrag suchen $sqlStz = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel"; $sqlStz .= " WHERE fk_commande = ".((int)$order->id); $sqlStz .= " AND status = 0"; // Nur Entwürfe $sqlStz .= " ORDER BY date_stundenzettel DESC, rowid DESC"; $sqlStz .= " LIMIT 1"; $resqlStz = $db->query($sqlStz); if ($resqlStz && $db->num_rows($resqlStz) > 0) { $objStz = $db->fetch_object($resqlStz); $notesStundenzettel = new Stundenzettel($db); $notesStundenzettel->fetch($objStz->rowid); } } // Notizen aus dem Stundenzettel laden (falls vorhanden) if ($notesStundenzettel && $notesStundenzettel->id > 0) { $notesStundenzettel->fetchNotes(); $notesToShow = $notesStundenzettel->notes; } // Merkzettel anzeigen, wenn welche vorhanden sind if (count($notesToShow) > 0) { print '
'; print ''.img_picto('', 'list', 'class="pictofixedwidth"').$langs->trans("NotesMemo").''; print ' ('.$notesStundenzettel->ref.')'; print ''; print '
'; } // ============================================= // TAB: ALLE STUNDENZETTEL // ============================================= if ($tab == 'stundenzettel') { // Auftrag-ID aus Stundenzettel verwenden wenn vorhanden, sonst aus URL // Wichtig: Gleiche Logik wie in lib/stundenzettel.lib.php für Badge-Berechnung $orderIdForQuery = $stundenzettelObj ? (int)$stundenzettelObj->fk_commande : (int)$order->id; $sql = "SELECT s.rowid, s.ref, s.date_stundenzettel, s.status, s.fk_user_author,"; $sql .= " u.firstname, u.lastname,"; $sql .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp WHERE sp.fk_stundenzettel = s.rowid AND sp.origin IN ('order', 'added')), 0) as total_qty_products,"; $sql .= " COALESCE((SELECT SUM(sl.duration) FROM ".MAIN_DB_PREFIX."stundenzettel_leistung sl WHERE sl.fk_stundenzettel = s.rowid), 0) as total_duration_minutes"; $sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel as s"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as u ON u.rowid = s.fk_user_author"; $sql .= " WHERE s.fk_commande = ".((int)$orderIdForQuery); $sql .= " ORDER BY s.date_stundenzettel DESC, s.rowid DESC"; $resqlStzList = $db->query($sql); $numStz = $resqlStzList ? $db->num_rows($resqlStzList) : 0; print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; if ($numStz > 0) { while ($objStz = $db->fetch_object($resqlStzList)) { $isCurrentStz = ($stundenzettel_id > 0 && $objStz->rowid == $stundenzettel_id); print ''; // Ref print ''; // Datum print ''; // Autor print ''; // Gesamtmenge Produkte print ''; // Gesamtzeit Leistungen print ''; // Status $stzTemp = new Stundenzettel($db); print ''; print ''; } } else { print ''; } print '
'.$langs->trans("Ref").''.$langs->trans("Date").''.$langs->trans("Author").''.$langs->trans("Products").''.$langs->trans("LeistungDuration").''.$langs->trans("Status").'
'; print ''; print img_picto('', 'clock', 'class="pictofixedwidth"').$objStz->ref; print ''; if ($isCurrentStz) { print ' aktuell'; } print ''.dol_print_date($db->jdate($objStz->date_stundenzettel), 'day').''.$objStz->firstname.' '.$objStz->lastname.''; if ($objStz->total_qty_products > 0) { print formatQty($objStz->total_qty_products); } else { print '-'; } print ''; if ($objStz->total_duration_minutes > 0) { $hours = floor($objStz->total_duration_minutes / 60); $minutes = $objStz->total_duration_minutes % 60; print sprintf('%d:%02d h', $hours, $minutes); } else { print '-'; } print ''.$stzTemp->LibStatut($objStz->status, 5).'
'.$langs->trans("NoRecordFound").'
'; print '
'; // Button: Neuen Stundenzettel erstellen print '
'; print ''; print img_picto('', 'add', 'class="pictofixedwidth"').$langs->trans("CreateStundenzettel"); print ''; print '
'; } // ============================================= // TAB: PRODUKTLISTE AUS AUFTRAG // ============================================= if ($tab == 'products') { // Farben für Sections (wie im SubtotalTitle-Modul) $sectionColors = array('#4a90d9', '#50b87d', '#e67e22', '#9b59b6', '#e74c3c', '#1abc9c', '#f39c12', '#3498db'); // Filter-Dropdown mit localStorage-Speicherung pro Auftrag print '
'; // Filter print '
'; print ''; print ''; print '
'; // Alle ein-/ausklappen Buttons print '
'; print ''; print ' '; print ''; print '
'; print '
'; // JavaScript für Filter-Speicherung pro Auftrag print ''; print '
'; print ''; print ''; if ($stundenzettel_id > 0) { print ''; } // Datum für Stundenzettel nur anzeigen, wenn kein Stundenzettel ausgewählt if (!$stundenzettel_id) { // Standard-Datum ermitteln basierend auf Einstellung $defaultDateSetting = getDolGlobalString('STUNDENZETTEL_DEFAULT_DATE', 'today'); $defaultDate = date('Y-m-d'); if ($defaultDateSetting == 'last_open') { // Datum des letzten offenen Stundenzettels für diesen Auftrag suchen $sqlLastOpen = "SELECT date_stundenzettel FROM ".MAIN_DB_PREFIX."stundenzettel"; $sqlLastOpen .= " WHERE fk_commande = ".((int)$order->id); $sqlLastOpen .= " AND status = 0"; // Nur Entwürfe $sqlLastOpen .= " ORDER BY date_stundenzettel DESC, rowid DESC LIMIT 1"; $resqlLastOpen = $db->query($sqlLastOpen); if ($resqlLastOpen && $db->num_rows($resqlLastOpen) > 0) { $objLastOpen = $db->fetch_object($resqlLastOpen); if ($objLastOpen->date_stundenzettel) { $defaultDate = date('Y-m-d', $db->jdate($objLastOpen->date_stundenzettel)); } } } print '
'; print ' '; print ''; print '
'; } else { // Prüfen ob der ausgewählte Stundenzettel von heute ist $stzDateStr = date('Y-m-d', $stundenzettelObj->date_stundenzettel); $todayStr = date('Y-m-d'); print '
'; if ($stzDateStr == $todayStr) { // Stundenzettel ist von heute - normal anzeigen print ''.$langs->trans("TransferToStundenzettel").': '; print ''.$stundenzettelObj->ref.' ('.dol_print_date($stundenzettelObj->date_stundenzettel, 'day').')'; } else { // Stundenzettel ist von einem anderen Tag - Hinweis dass neuer erstellt wird print ''.$langs->trans("TransferToStundenzettel").': '; print ''.$langs->trans("NewStundenzettelForToday").''; print ' ('.dol_print_date(dol_now(), 'day').')'; print '
'.$langs->trans("SelectedStundenzettelNotToday", $stundenzettelObj->ref, dol_print_date($stundenzettelObj->date_stundenzettel, 'day')).''; // Hidden field für das heutige Datum print ''; print ''; } print '
'; } print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; // Lade Sections und Produkte aus facture_lines_manager $sections = array(); $products_by_section = array(); $products_without_section = array(); // Erst alle Sections laden $sql = "SELECT m.rowid, m.title, m.line_order"; $sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager as m"; $sql .= " WHERE m.fk_commande = ".((int)$order->id); $sql .= " AND m.document_type = 'order'"; $sql .= " AND m.line_type = 'section'"; $sql .= " ORDER BY m.line_order"; $resql = $db->query($sql); if ($resql) { $colorIndex = 0; while ($obj = $db->fetch_object($resql)) { $sections[$obj->rowid] = array( 'title' => $obj->title, 'line_order' => $obj->line_order, 'color' => $sectionColors[$colorIndex % count($sectionColors)] ); $products_by_section[$obj->rowid] = array(); $colorIndex++; } } // Dann alle Produkte laden mit Section-Zuordnung // Berechne qty_delivered, qty_added (Mehraufwand) und qty_removed (Entfällt) direkt aus Stundenzetteln $sql = "SELECT m.rowid as manager_id, m.fk_commandedet, m.parent_section, m.line_order,"; $sql .= " cd.rowid, cd.fk_product, cd.qty, cd.description,"; $sql .= " p.ref as product_ref, p.label as product_label,"; // qty_delivered: Summe aller qty_done für diese Auftragszeile (origin = 'order' oder 'added') $sql .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; $sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; $sql .= " WHERE sp.fk_commandedet = cd.rowid AND sp.origin IN ('order', 'added')), 0) as qty_delivered,"; // qty_added: Mehraufwand für dieses Produkt (origin = 'additional') $sql .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2"; $sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel"; $sql .= " WHERE sp2.fk_product = cd.fk_product AND sp2.origin = 'additional' AND s2.fk_commande = ".((int)$order->id)."), 0) as qty_added,"; // qty_removed: Entfällt für dieses Produkt (origin = 'omitted') $sql .= " COALESCE((SELECT SUM(sp3.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp3"; $sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s3 ON s3.rowid = sp3.fk_stundenzettel"; $sql .= " WHERE sp3.fk_product = cd.fk_product AND sp3.origin = 'omitted' AND s3.fk_commande = ".((int)$order->id)."), 0) as qty_removed"; $sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager as m"; $sql .= " JOIN ".MAIN_DB_PREFIX."commandedet as cd ON cd.rowid = m.fk_commandedet"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product"; $sql .= " WHERE m.fk_commande = ".((int)$order->id); $sql .= " AND m.document_type = 'order'"; $sql .= " AND m.line_type = 'product'"; $sql .= " ORDER BY m.line_order"; $resql = $db->query($sql); if ($resql) { while ($obj = $db->fetch_object($resql)) { if ($obj->parent_section && isset($products_by_section[$obj->parent_section])) { $products_by_section[$obj->parent_section][] = $obj; } else { $products_without_section[] = $obj; } } } // Falls keine Manager-Einträge, direkt aus commandedet laden $hasManagerData = (count($sections) > 0 || count($products_without_section) > 0); if (!$hasManagerData) { // Berechne qty_delivered, qty_added (Mehraufwand) und qty_removed (Entfällt) direkt aus Stundenzetteln $sql = "SELECT cd.rowid, cd.fk_product, cd.qty, cd.description,"; $sql .= " p.ref as product_ref, p.label as product_label,"; // qty_delivered: Summe aller qty_done für diese Auftragszeile (origin = 'order' oder 'added') $sql .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; $sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; $sql .= " WHERE sp.fk_commandedet = cd.rowid AND sp.origin IN ('order', 'added')), 0) as qty_delivered,"; // qty_added: Mehraufwand für dieses Produkt (origin = 'additional') $sql .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2"; $sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel"; $sql .= " WHERE sp2.fk_product = cd.fk_product AND sp2.origin = 'additional' AND s2.fk_commande = ".((int)$order->id)."), 0) as qty_added,"; // qty_removed: Entfällt für dieses Produkt (origin = 'omitted') $sql .= " COALESCE((SELECT SUM(sp3.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp3"; $sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s3 ON s3.rowid = sp3.fk_stundenzettel"; $sql .= " WHERE sp3.fk_product = cd.fk_product AND sp3.origin = 'omitted' AND s3.fk_commande = ".((int)$order->id)."), 0) as qty_removed"; $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product"; $sql .= " WHERE cd.fk_commande = ".((int)$order->id); $sql .= " AND (cd.fk_product > 0 OR (cd.fk_product = 0 AND cd.description IS NOT NULL AND cd.description != ''))"; $sql .= " AND (cd.special_code IS NULL OR cd.special_code = 0)"; // Keine SubtotalTitle-Spezialzeilen $sql .= " ORDER BY cd.rang"; $resql = $db->query($sql); if ($resql) { while ($obj = $db->fetch_object($resql)) { $products_without_section[] = $obj; } } } // Bereits auf dem ausgewählten Stundenzettel vorhandene Produkte laden // NUR wenn der Stundenzettel von heute ist - ansonsten wird ein neuer erstellt $alreadyOnStundenzettel = array(); $stzIsToday = false; if ($stundenzettel_id > 0 && $stundenzettelObj) { $stzDateStr = date('Y-m-d', $stundenzettelObj->date_stundenzettel); $todayStr = date('Y-m-d'); $stzIsToday = ($stzDateStr == $todayStr); // Nur wenn der Stundenzettel von heute ist, die bereits vorhandenen Produkte laden if ($stzIsToday) { $sqlExisting = "SELECT sp.fk_commandedet FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; $sqlExisting .= " WHERE sp.fk_stundenzettel = ".((int)$stundenzettel_id); $sqlExisting .= " AND sp.fk_commandedet IS NOT NULL AND sp.fk_commandedet > 0"; $resqlExisting = $db->query($sqlExisting); if ($resqlExisting) { while ($objExisting = $db->fetch_object($resqlExisting)) { $alreadyOnStundenzettel[$objExisting->fk_commandedet] = true; } } } } // Hilfsfunktion für Produktzeile - gibt true zurück wenn angezeigt, false wenn übersprungen $printProductRow = function($obj, $color = null, $sectionId = null) use ($langs, $filter, $alreadyOnStundenzettel) { $qty_added = isset($obj->qty_added) ? (float)$obj->qty_added : 0; $qty_removed = isset($obj->qty_removed) ? (float)$obj->qty_removed : 0; // Effektive Gesamtmenge = Original + Hinzugefügt - Entfallen $effectiveTotal = $obj->qty + $qty_added - $qty_removed; $remaining = $effectiveTotal - $obj->qty_delivered; $isDone = ($remaining <= 0); // Filter anwenden if ($filter == 'open' && $isDone) { return false; } if ($filter == 'done' && !$isDone) { return false; } // 'all' zeigt alles // Styling: Nur erste Zelle bekommt border-left $styleFirst = $color ? ' style="border-left: 4px solid '.$color.';"' : ''; $sectionClass = $sectionId ? ' section-product section_'.$sectionId : ''; print ''; // Checkbox (nur bei offenen anzeigen UND wenn noch nicht auf diesem Stundenzettel) $isAlreadyOnStz = isset($alreadyOnStundenzettel[$obj->rowid]); print ''; // Produkt print ''; // Menge bestellt - zeigt Gesamtzahl mit Info über Änderungen print ''; // Menge geliefert/verbaut print ''; // Verbleibend print ''; // Status print ''; print ''; return true; }; $totalProducts = 0; // Sections mit Produkten anzeigen foreach ($sections as $sectionId => $section) { $sectionProducts = $products_by_section[$sectionId]; if (count($sectionProducts) == 0) continue; // Prüfen, wie viele Produkte nach Filter angezeigt werden $visibleProductsInSection = 0; foreach ($sectionProducts as $prod) { $prodQtyAdded = isset($prod->qty_added) ? (float)$prod->qty_added : 0; $prodQtyRemoved = isset($prod->qty_removed) ? (float)$prod->qty_removed : 0; $prodEffectiveTotal = $prod->qty + $prodQtyAdded - $prodQtyRemoved; $remaining = $prodEffectiveTotal - $prod->qty_delivered; $isDone = ($remaining <= 0); // Filter-Logik if ($filter == 'open' && !$isDone) { $visibleProductsInSection++; } elseif ($filter == 'done' && $isDone) { $visibleProductsInSection++; } elseif ($filter == 'all') { $visibleProductsInSection++; } } // Section nur anzeigen wenn Produkte nach Filter vorhanden if ($visibleProductsInSection == 0) continue; // Section-Header mit Buffering, wird nur ausgegeben wenn Produkte angezeigt werden // Dezente Farbgebung: nur linker Rand farbig, heller Hintergrund $sectionHeader = ''; $sectionHeader .= ''; $sectionHeader .= ''; print $sectionHeader; // Produkte dieser Section foreach ($sectionProducts as $prod) { if ($printProductRow($prod, $section['color'], $sectionId)) { $totalProducts++; } } } // Produkte ohne Section $visibleProductsWithoutSection = 0; foreach ($products_without_section as $prod) { $prodQtyAdded = isset($prod->qty_added) ? (float)$prod->qty_added : 0; $prodQtyRemoved = isset($prod->qty_removed) ? (float)$prod->qty_removed : 0; $prodEffectiveTotal = $prod->qty + $prodQtyAdded - $prodQtyRemoved; $remaining = $prodEffectiveTotal - $prod->qty_delivered; $isDone = ($remaining <= 0); // Filter-Logik if ($filter == 'open' && !$isDone) { $visibleProductsWithoutSection++; } elseif ($filter == 'done' && $isDone) { $visibleProductsWithoutSection++; } elseif ($filter == 'all') { $visibleProductsWithoutSection++; } } if ($visibleProductsWithoutSection > 0) { if (count($sections) > 0) { // Trennzeile nur wenn es auch Sections gibt - einklappbar, dezente Farbgebung print ''; print ''; print ''; } foreach ($products_without_section as $prod) { if ($printProductRow($prod, null, 'other')) { $totalProducts++; } } } if ($totalProducts == 0) { print ''; } // ============================================= // BEREICH: MEHRAUFWAND-PRODUKTE (aus allen Stundenzetteln dieses Auftrags) // ============================================= // Mehraufwand umfasst: // 1. origin='additional' → Mehraufwand-Bestellung (Beauftragt) // 2. origin='added' ohne fk_commandedet → Produkt nicht im Auftrag, in Produktliste hinzugefügt (Verbaut) // Logik: // - Wenn NUR 'added' existiert: Beauftragt = Verbaut = Menge // - Wenn 'additional' existiert: Beauftragt von 'additional', Verbaut von 'added' $sqlMehraufwand = "SELECT sp.fk_product, sp.product_ref, sp.product_label, sp.description,"; // qty_additional = Menge aus Mehraufwand-Zeilen $sqlMehraufwand .= " SUM(CASE WHEN sp.origin = 'additional' THEN sp.qty_done ELSE 0 END) as qty_additional,"; // qty_added = Menge aus Produktliste (nicht im Auftrag) $sqlMehraufwand .= " SUM(CASE WHEN sp.origin = 'added' AND (sp.fk_commandedet IS NULL OR sp.fk_commandedet = 0) THEN sp.qty_done ELSE 0 END) as qty_added,"; $sqlMehraufwand .= " GROUP_CONCAT(DISTINCT s.ref SEPARATOR ', ') as stundenzettel_refs"; $sqlMehraufwand .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; $sqlMehraufwand .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; $sqlMehraufwand .= " WHERE s.fk_commande = ".((int)$order->id); // Beide Typen: Mehraufwand ODER hinzugefügt ohne Auftragszeile $sqlMehraufwand .= " AND (sp.origin = 'additional' OR (sp.origin = 'added' AND (sp.fk_commandedet IS NULL OR sp.fk_commandedet = 0)))"; $sqlMehraufwand .= " GROUP BY sp.fk_product, sp.product_ref, sp.product_label, sp.description"; $sqlMehraufwand .= " ORDER BY sp.product_ref, sp.description"; $resqlMehraufwand = $db->query($sqlMehraufwand); $mehraufwandCount = 0; if ($resqlMehraufwand) { $mehraufwandCount = $db->num_rows($resqlMehraufwand); } if ($mehraufwandCount > 0) { // Mehraufwand-Header - einklappbar, dezente Farbgebung print ''; print ''; print ''; while ($objMa = $db->fetch_object($resqlMehraufwand)) { // Logik für Beauftragt und Verbaut: // - qty_additional = Menge aus Mehraufwand-Zeilen (origin='additional') // - qty_added = Menge aus Produktliste ohne Auftragszeile (origin='added', kein fk_commandedet) // Wenn NUR 'added' existiert: Beauftragt = Verbaut = qty_added // Wenn 'additional' existiert: Beauftragt = qty_additional, Verbaut = qty_added $qtyAdditional = (float)$objMa->qty_additional; $qtyAdded = (float)$objMa->qty_added; // Beauftragt: Wenn Mehraufwand existiert, dessen Menge nehmen, sonst die hinzugefügte Menge $qtyTarget = ($qtyAdditional > 0) ? $qtyAdditional : $qtyAdded; // Verbaut: Immer die hinzugefügte Menge (was tatsächlich installiert wurde) $qtyDone = $qtyAdded; $qtyRemaining = $qtyTarget - $qtyDone; // Verbleibend // Product IDs für Checkbox holen (beide origin-Typen) $sqlIds = "SELECT GROUP_CONCAT(DISTINCT sp.rowid ORDER BY sp.rowid) as product_ids"; $sqlIds .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; $sqlIds .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; $sqlIds .= " WHERE s.fk_commande = ".((int)$order->id); $sqlIds .= " AND (sp.origin = 'additional' OR (sp.origin = 'added' AND (sp.fk_commandedet IS NULL OR sp.fk_commandedet = 0)))"; if ($objMa->fk_product > 0) { $sqlIds .= " AND sp.fk_product = ".((int)$objMa->fk_product); } else { $sqlIds .= " AND sp.fk_product IS NULL AND sp.description = '".$db->escape($objMa->description)."'"; } $resqlIds = $db->query($sqlIds); $productIds = ''; if ($resqlIds && ($objIds = $db->fetch_object($resqlIds))) { $productIds = $objIds->product_ids; } $isDone = ($qtyRemaining <= 0 && $qtyDone > 0); print ''; // Checkbox - Mehraufwand immer anzeigen (für Auswahl/Übersicht) print ''; // Produkt print ''; // Menge beauftragt (Zielmenge - was verbaut werden soll) print ''; // Menge tatsächlich verbaut print ''; // Verbleibend (Zielmenge - Verbaut) print ''; // Status print ''; print ''; } } // HINWEIS: Entfällt-Produkte werden nicht separat angezeigt // Die Entfällt-Mengen werden bereits in der Beauftragt-Spalte der normalen Produkte // als Abzug berücksichtigt (effectiveTotal = qty + qty_added - qty_removed) print '
'.$langs->trans("Product").''.$langs->trans("QtyOrdered").''.$langs->trans("QtyDelivered").''.$langs->trans("QtyRemaining").''.$langs->trans("Status").'
'; if (!$isDone && !$isAlreadyOnStz) { print ''; } elseif ($isAlreadyOnStz) { print ''; } print ''; if ($obj->fk_product > 0) { print ''; print img_picto('', 'product', 'class="pictofixedwidth"'); print $obj->product_ref.' - '.$obj->product_label; print ''; } else { // Freitext-Produkt: Beschreibung anzeigen print img_picto('', 'generic', 'class="pictofixedwidth"'); $desc = !empty($obj->description) ? strip_tags($obj->description) : '-'; // Beschreibung kürzen wenn zu lang if (strlen($desc) > 80) { $desc = substr($desc, 0, 77).'...'; } print ''.$desc.''; } print ''; // Gesamtzahl (effektiv) als Hauptzahl print ''.formatQty($effectiveTotal).''; // Änderungen als kleine Info-Badges if ($qty_added > 0) { print ' +'.formatQty($qty_added).''; } if ($qty_removed > 0) { print ' -'.formatQty($qty_removed).''; } print ''.formatQty($obj->qty_delivered).''; if ($remaining > 0) { print ''.formatQty($remaining).''; } elseif ($remaining == 0) { print '0'; } else { // Mehr verbaut als bestellt print ''.formatQty($remaining).''; } print ''; if ($isDone) { print ''.$langs->trans("TrackingDone").''; } elseif ($obj->qty_delivered > 0) { print ''.$langs->trans("TrackingPartial").''; } else { print ''.$langs->trans("TrackingOpen").''; } print '
'; $sectionHeader .= ''; $sectionHeader .= dol_escape_htmltag($section['title']); $sectionHeader .= ' ('.$visibleProductsInSection.' '.$langs->trans("Products").')'; $sectionHeader .= '
'; print ''; print $langs->trans("OtherProducts").' ('.$visibleProductsWithoutSection.' '.$langs->trans("Products").')'; print '
'.$langs->trans("AllProductsDocumented").'
'; print ''; print ''; print $langs->trans("Mehraufwand"); print ' ('.$mehraufwandCount.' '.$langs->trans("Products").')'; print '
'; if ($productIds) { print ''; } print ''; if ($objMa->fk_product > 0) { print ''; print img_picto('', 'product', 'class="pictofixedwidth"'); print $objMa->product_ref.' - '.$objMa->product_label; print ''; } else { print img_picto('', 'generic', 'class="pictofixedwidth"'); $desc = !empty($objMa->description) ? strip_tags($objMa->description) : $langs->trans("FreeText"); if (strlen($desc) > 80) { $desc = substr($desc, 0, 77).'...'; } print ''.$desc.''; } print ' '.$langs->trans("Mehraufwand").''; print ''.formatQty($qtyTarget).''.formatQty($qtyDone).''; if ($qtyRemaining > 0) { print ''.formatQty($qtyRemaining).''; } elseif ($qtyRemaining < 0) { // Mehr verbaut als geplant print ''.formatQty($qtyRemaining).''; } else { print '0'; } print ''; if ($qtyDone > 0 && $qtyRemaining > 0) { print ''.$langs->trans("TrackingPartial").''; } elseif ($qtyDone == 0) { print ''.$langs->trans("TrackingOpen").''; } else { print ''.$langs->trans("TrackingDone").''; } print ' ('.$langs->trans("FromStundenzettel").')'; print '
'; print '
'; // Button nur anzeigen wenn nicht freigegeben if ($stundenzettelStatus == 0) { print '
'; print ''; print '
'; } elseif ($stundenzettelStatus == 1) { // Status 1: Freigegeben - zeige Meldung mit Wiedereröffnen-Button print '
'; print img_picto('', 'lock', 'class="pictofixedwidth"'); print $langs->trans("StundenzettelReleasedNoChanges"); print ' '; print img_picto('', 'unlock', 'class="pictofixedwidth"').$langs->trans("ReopenStundenzettel"); print ''; print '
'; } else { // Status 2: In Rechnung übertragen print '
'; print img_picto('', 'check', 'class="pictofixedwidth"'); print $langs->trans("StundenzettelTransferredToInvoice"); print ' '; print img_picto('', 'undo', 'class="pictofixedwidth"').$langs->trans("ResetStundenzettel"); print ''; print '
'; } print '
'; // JavaScript für "Alle auswählen" und Section-Toggle print ''; // ============================================= // AKTIONSBUTTONS FÜR STUNDENZETTEL-FREIGABE // ============================================= if ($stundenzettelStatus < 2) { print '
'; if ($stundenzettelStatus == 0) { // Status: Offen - Zeige Freigeben-Button wenn Stundenzettel vorhanden if ($hasStundenzettel && $allValidated) { if ($hasRemainingProducts) { // Mit Warnung print ''; print img_picto('', 'lock', 'class="pictofixedwidth"').$langs->trans("ReleaseStundenzettel"); print ''; } else { print ''; print img_picto('', 'lock', 'class="pictofixedwidth"').$langs->trans("ReleaseStundenzettel"); print ''; } } elseif ($hasStundenzettel && !$allValidated) { print ''; print img_picto('', 'lock', 'class="pictofixedwidth"').$langs->trans("ReleaseStundenzettel"); print ''; } } elseif ($stundenzettelStatus == 1) { // Status: Freigegeben - Zeige Wiedereröffnen und Rechnung-Button print ''; print img_picto('', 'unlock', 'class="pictofixedwidth"').$langs->trans("ReopenStundenzettel"); print ''; if ($user->hasRight('facture', 'creer')) { print ''; print img_picto('', 'bill', 'class="pictofixedwidth"').$langs->trans("TransferToInvoice"); print ''; } } print '
'; } } // ============================================= // TAB: LIEFERAUFLISTUNG / TRACKING // ============================================= if ($tab == 'tracking') { $total_ordered = 0; $total_delivered = 0; $total_remaining = 0; // Alle Stundenzettel-Details pro Produkt laden (für ausklappbare Ansicht) $trackingDetails = array(); // Array[fk_commandedet] => array of entries $sqlDetails = "SELECT sp.rowid, sp.fk_stundenzettel, sp.fk_commandedet, sp.fk_product,"; $sqlDetails .= " sp.qty_done, sp.origin, sp.description,"; $sqlDetails .= " s.ref as stz_ref, s.date_stundenzettel"; $sqlDetails .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; $sqlDetails .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; $sqlDetails .= " WHERE s.fk_commande = ".((int)$order->id); $sqlDetails .= " AND sp.qty_done > 0"; $sqlDetails .= " ORDER BY s.date_stundenzettel DESC, sp.rowid"; $resqlDetails = $db->query($sqlDetails); if ($resqlDetails) { while ($objD = $db->fetch_object($resqlDetails)) { // Key-Logik: fk_commandedet > prod_X > freetext_hash if ($objD->fk_commandedet > 0) { $key = $objD->fk_commandedet; } elseif ($objD->fk_product > 0) { $key = 'prod_'.$objD->fk_product; } else { // Freitext: Hash der Beschreibung als Key $key = 'freetext_'.md5(trim($objD->description)); } if (!isset($trackingDetails[$key])) { $trackingDetails[$key] = array(); } $trackingDetails[$key][] = $objD; } } // Buttons für Details anzeigen/verbergen print '
'; print ''; print ''; print '
'; print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; // Live-Berechnung aus allen Stundenzetteln (inkl. Entwürfe) $sql = "SELECT cd.rowid, cd.fk_product, cd.qty as qty_ordered, cd.description,"; $sql .= " p.ref as product_ref, p.label as product_label,"; $sql .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; $sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; $sql .= " WHERE sp.fk_commandedet = cd.rowid AND sp.origin IN ('order', 'added')), 0) as qty_delivered,"; // Mehraufwand - nur für Produkte mit fk_product > 0 (Freitext-Mehraufwand wird separat angezeigt) $sql .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2"; $sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel"; $sql .= " WHERE sp2.fk_product = cd.fk_product AND cd.fk_product > 0 AND sp2.origin = 'additional' AND s2.fk_commande = ".((int)$order->id)."), 0) as qty_additional,"; // Entfällt - für Produkte via fk_product, für Freitext via fk_commandedet $sql .= " COALESCE((SELECT SUM(sp3.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp3"; $sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s3 ON s3.rowid = sp3.fk_stundenzettel"; $sql .= " WHERE sp3.origin = 'omitted' AND s3.fk_commande = ".((int)$order->id); $sql .= " AND ((sp3.fk_product = cd.fk_product AND cd.fk_product > 0) OR sp3.fk_commandedet = cd.rowid)), 0) as qty_omitted"; $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product"; $sql .= " WHERE cd.fk_commande = ".((int)$order->id); $sql .= " AND (cd.fk_product > 0 OR (cd.fk_product = 0 AND cd.description IS NOT NULL AND cd.description != ''))"; $sql .= " AND (cd.special_code IS NULL OR cd.special_code = 0)"; $sql .= " ORDER BY cd.rang"; $resql = $db->query($sql); $detailRowId = 0; if ($resql) { $num = $db->num_rows($resql); if ($num > 0) { while ($obj = $db->fetch_object($resql)) { $detailRowId++; $qty_additional = (float)$obj->qty_additional; $qty_omitted = (float)$obj->qty_omitted; $effective_ordered = $obj->qty_ordered + $qty_additional - $qty_omitted; $qty_remaining = $effective_ordered - $obj->qty_delivered; // Details für dieses Produkt $details = isset($trackingDetails[$obj->rowid]) ? $trackingDetails[$obj->rowid] : array(); // Falls keine Details über fk_commandedet, versuche über fk_product if (empty($details) && $obj->fk_product > 0) { $prodKey = 'prod_'.$obj->fk_product; if (isset($trackingDetails[$prodKey])) { $details = $trackingDetails[$prodKey]; } } $hasDetails = !empty($details); print ''; // Produkt mit Toggle print ''; // Bestellt (mit Badges für Mehraufwand/Entfällt) print ''; $total_ordered += $effective_ordered; // Geliefert/Erfasst print ''; $total_delivered += $obj->qty_delivered; // Verbleibend print ''; $total_remaining += $qty_remaining; // Status print ''; print ''; // Detail-Zeile (standardmäßig eingeklappt) if ($hasDetails) { print ''; print ''; print ''; } } // Summenzeile print ''; print ''; print ''; print ''; print ''; print ''; print ''; } else { print ''; } } // ============================================= // ZUSÄTZLICHE PRODUKTE: Mehraufwand die NICHT im Auftrag sind // (Produkte und Freitext die direkt als Mehraufwand hinzugefügt wurden) // ============================================= // Alle Produkt-IDs aus dem Auftrag sammeln (für Ausschluss) $orderProductIds = array(); $sqlOrderProds = "SELECT fk_product FROM ".MAIN_DB_PREFIX."commandedet WHERE fk_commande = ".((int)$order->id)." AND fk_product > 0"; $resOrderProds = $db->query($sqlOrderProds); if ($resOrderProds) { while ($objOP = $db->fetch_object($resOrderProds)) { $orderProductIds[] = $objOP->fk_product; } } // Mehraufwand-Produkte laden die NICHT im Auftrag sind $sqlMehraufwand = "SELECT sp.fk_product, sp.description,"; $sqlMehraufwand .= " p.ref as product_ref, p.label as product_label,"; $sqlMehraufwand .= " SUM(sp.qty) as qty_ordered,"; $sqlMehraufwand .= " SUM(sp.qty_done) as qty_delivered"; $sqlMehraufwand .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; $sqlMehraufwand .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; $sqlMehraufwand .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = sp.fk_product"; $sqlMehraufwand .= " WHERE s.fk_commande = ".((int)$order->id); $sqlMehraufwand .= " AND sp.origin = 'additional'"; if (!empty($orderProductIds)) { $sqlMehraufwand .= " AND (sp.fk_product NOT IN (".implode(',', $orderProductIds).") OR sp.fk_product = 0)"; } $sqlMehraufwand .= " GROUP BY sp.fk_product, sp.description, p.ref, p.label"; $sqlMehraufwand .= " ORDER BY p.ref, sp.description"; $resqlMehr = $db->query($sqlMehraufwand); $hasMehraufwandProducts = false; if ($resqlMehr && $db->num_rows($resqlMehr) > 0) { // Separator-Zeile für Mehraufwand print ''; print ''; print ''; while ($objMehr = $db->fetch_object($resqlMehr)) { $hasMehraufwandProducts = true; $detailRowId++; $qty_ordered_mehr = (float)$objMehr->qty_ordered; $qty_delivered_mehr = (float)$objMehr->qty_delivered; $qty_remaining_mehr = $qty_ordered_mehr - $qty_delivered_mehr; // Details für Mehraufwand laden $detailsMehr = array(); if ($objMehr->fk_product > 0) { $prodKey = 'prod_'.$objMehr->fk_product; if (isset($trackingDetails[$prodKey])) { $detailsMehr = $trackingDetails[$prodKey]; } } else { // Freitext: Hash der Beschreibung als Key $freetextKey = 'freetext_'.md5(trim($objMehr->description)); if (isset($trackingDetails[$freetextKey])) { $detailsMehr = $trackingDetails[$freetextKey]; } } $hasDetailsMehr = !empty($detailsMehr); print ''; // Produkt/Beschreibung print ''; // Bestellt (Mehraufwand-Menge) print ''; $total_ordered += $qty_ordered_mehr; // Geliefert print ''; $total_delivered += $qty_delivered_mehr; // Verbleibend print ''; $total_remaining += $qty_remaining_mehr; // Status print ''; print ''; // Detail-Zeile für Mehraufwand if ($hasDetailsMehr) { print ''; print ''; print ''; } } // Neue Summenzeile mit Mehraufwand print ''; print ''; print ''; print ''; print ''; print ''; print ''; } print '
'.$langs->trans("Product").''.$langs->trans("QtyOrdered").''.$langs->trans("QtyDelivered").''.$langs->trans("QtyRemaining").''.$langs->trans("Status").'
'; if ($hasDetails) { print ''; print ''; print ''; } if ($obj->fk_product > 0) { print ''; print img_picto('', 'product', 'class="pictofixedwidth"'); print $obj->product_ref.' - '.$obj->product_label; print ''; } else { print img_picto('', 'generic', 'class="pictofixedwidth"'); $desc = !empty($obj->description) ? strip_tags($obj->description) : '-'; if (strlen($desc) > 60) $desc = substr($desc, 0, 57).'...'; print ''.$desc.''; } print ''; print ''.formatQty($effective_ordered).''; if ($qty_additional > 0) { print ' +'.formatQty($qty_additional).''; } if ($qty_omitted > 0) { print ' -'.formatQty($qty_omitted).''; } print ''.formatQty($obj->qty_delivered).''; if ($qty_remaining > 0) { print ''.formatQty($qty_remaining).''; } elseif ($qty_remaining == 0) { print '0'; } else { print ''.formatQty($qty_remaining).''; } print ''; if ($qty_remaining <= 0) { print ''.$langs->trans("TrackingDone").''; } elseif ($obj->qty_delivered > 0) { print ''.$langs->trans("TrackingPartial").''; } else { print ''.$langs->trans("TrackingOpen").''; } print '
'.$langs->trans("Total").''.formatQty($total_ordered).''.formatQty($total_delivered).''.formatQty($total_remaining).'
'.$langs->trans("NoRecordFound").'
'.$langs->trans("Mehraufwand").' ('.$langs->trans("MehraufwandDesc").')
'; if ($hasDetailsMehr) { print ''; print ''; print ''; } print ''.$langs->trans("Mehraufwand").''; if ($objMehr->fk_product > 0) { print ''; print img_picto('', 'product', 'class="pictofixedwidth"'); print $objMehr->product_ref.' - '.$objMehr->product_label; print ''; } else { // Freitext print img_picto('', 'generic', 'class="pictofixedwidth"'); $desc = !empty($objMehr->description) ? strip_tags($objMehr->description) : '-'; if (strlen($desc) > 50) $desc = substr($desc, 0, 47).'...'; print ''.$desc.''; } print ''.formatQty($qty_ordered_mehr).''.formatQty($qty_delivered_mehr).''; if ($qty_remaining_mehr > 0) { print ''.formatQty($qty_remaining_mehr).''; } elseif ($qty_remaining_mehr == 0) { print '0'; } else { print ''.formatQty($qty_remaining_mehr).''; } print ''; if ($qty_remaining_mehr <= 0) { print ''.$langs->trans("TrackingDone").''; } elseif ($qty_delivered_mehr > 0) { print ''.$langs->trans("TrackingPartial").''; } else { print ''.$langs->trans("TrackingOpen").''; } print '
'.$langs->trans("Total").' ('.$langs->trans("Mehraufwand").' '.$langs->trans("incl").')'.formatQty($total_ordered).''.formatQty($total_delivered).''.formatQty($total_remaining).'
'; print '
'; // JavaScript für Tracking-Details print ''; // ============================================= // BEREICH: LEISTUNGEN / ARBEITSZEITEN // ============================================= print '
'; print '
'.$langs->trans("Leistungen").' / '.$langs->trans("TotalHours").'
'; // Leistungen nach Leistungsposition gruppiert laden $sqlLeistungen = "SELECT "; $sqlLeistungen .= " COALESCE(l.fk_product, 0) as service_id,"; $sqlLeistungen .= " p.ref as service_ref, p.label as service_label,"; $sqlLeistungen .= " SUM(l.duration) as total_minutes,"; $sqlLeistungen .= " COUNT(l.rowid) as entry_count"; $sqlLeistungen .= " FROM ".MAIN_DB_PREFIX."stundenzettel_leistung l"; $sqlLeistungen .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = l.fk_stundenzettel"; $sqlLeistungen .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = l.fk_product"; $sqlLeistungen .= " WHERE s.fk_commande = ".((int)$order->id); $sqlLeistungen .= " GROUP BY COALESCE(l.fk_product, 0), p.ref, p.label"; $sqlLeistungen .= " ORDER BY p.ref, p.label"; $resqlLeistungen = $db->query($sqlLeistungen); $totalMinutesAll = 0; print '
'; print ''; print ''; print ''; print ''; print ''; print ''; if ($resqlLeistungen) { $numLeistungen = $db->num_rows($resqlLeistungen); if ($numLeistungen > 0) { while ($objL = $db->fetch_object($resqlLeistungen)) { $totalMinutesAll += $objL->total_minutes; $hours = floor($objL->total_minutes / 60); $mins = $objL->total_minutes % 60; print ''; // Leistungsposition print ''; // Stunden print ''; // Anzahl Einträge print ''; print ''; } // Summenzeile $totalHours = floor($totalMinutesAll / 60); $totalMins = $totalMinutesAll % 60; print ''; print ''; print ''; print ''; print ''; } else { print ''; } } print '
'.$langs->trans("DefaultService").''.$langs->trans("TotalHours").''.$langs->trans("Entries").'
'; if ($objL->service_id > 0) { print ''; print img_picto('', 'service', 'class="pictofixedwidth"'); print $objL->service_ref.' - '.$objL->service_label; print ''; } else { print ''.img_picto('', 'service', 'class="pictofixedwidth"').$langs->trans("NotSet").''; } print ''.sprintf('%d:%02d h', $hours, $mins).''.$objL->entry_count.'
'.$langs->trans("Total").''.sprintf('%d:%02d h', $totalHours, $totalMins).'
'.$langs->trans("NoRecordFound").'
'; print '
'; // Details pro Stundenzettel print '
'; print '
'.$langs->trans("Leistungen").' '.$langs->trans("perStundenzettel").'
'; $sqlLeistDetail = "SELECT l.rowid, l.fk_stundenzettel, l.fk_product, l.date_leistung,"; $sqlLeistDetail .= " l.time_start, l.time_end, l.duration, l.description,"; $sqlLeistDetail .= " s.ref as stz_ref, s.date_stundenzettel,"; $sqlLeistDetail .= " p.ref as service_ref, p.label as service_label"; $sqlLeistDetail .= " FROM ".MAIN_DB_PREFIX."stundenzettel_leistung l"; $sqlLeistDetail .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = l.fk_stundenzettel"; $sqlLeistDetail .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = l.fk_product"; $sqlLeistDetail .= " WHERE s.fk_commande = ".((int)$order->id); $sqlLeistDetail .= " ORDER BY s.date_stundenzettel DESC, l.date_leistung, l.time_start"; $resqlLeistDetail = $db->query($sqlLeistDetail); print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; if ($resqlLeistDetail) { $numDetail = $db->num_rows($resqlLeistDetail); if ($numDetail > 0) { while ($objLD = $db->fetch_object($resqlLeistDetail)) { $hours = floor($objLD->duration / 60); $mins = $objLD->duration % 60; print ''; // Stundenzettel print ''; // Datum print ''; // Zeit print ''; // Dauer print ''; // Leistungsposition print ''; // Beschreibung print ''; print ''; } } else { print ''; } } print '
'.$langs->trans("Stundenzettel").''.$langs->trans("Date").''.$langs->trans("LeistungTimeStart").' - '.$langs->trans("LeistungTimeEnd").''.$langs->trans("LeistungDuration").''.$langs->trans("DefaultService").''.$langs->trans("Description").'
'; print ''; print $objLD->stz_ref; print ''; print ''.dol_print_date($db->jdate($objLD->date_leistung), 'day').''; if ($objLD->time_start && $objLD->time_end) { print substr($objLD->time_start, 0, 5).' - '.substr($objLD->time_end, 0, 5); } else { print '-'; } print ''.sprintf('%d:%02d h', $hours, $mins).''; if ($objLD->fk_product > 0) { print ''.$objLD->service_ref.''; } else { print '-'; } print ''; if (!empty($objLD->description)) { $desc = strip_tags($objLD->description); if (strlen($desc) > 50) $desc = substr($desc, 0, 47).'...'; print dol_escape_htmltag($desc); } else { print '-'; } print '
'.$langs->trans("NoRecordFound").'
'; print '
'; print '
'; // fichehalfleft // Button: In Rechnung übertragen (nur wenn alles erledigt) if ($total_remaining <= 0 && $total_delivered > 0) { print '
'; print ''; print img_picto('', 'bill', 'class="pictofixedwidth"').$langs->trans("TransferToInvoice"); print ''; print '
'; } } print '
'; // fichecenter dol_fiche_end(); llxFooter(); $db->close();