*
* 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").': ';
foreach ($remainingProducts as $prod) {
$productName = $prod->fk_product > 0 ? $prod->product_ref.' - '.$prod->product_label : strip_tags($prod->description);
$qtyRemaining = $prod->qty_ordered - $prod->qty_documented;
$remainingHtml .= ''.$productName.' - '.formatQty($qtyRemaining).' '.$langs->trans("QtyRemaining").' ';
}
$remainingHtml .= ' ';
$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 ''.img_picto('', 'service', 'class="pictofixedwidth"').$langs->trans("DefaultService").': ';
print '';
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 ' ';
print ' ';
// Geplante Stunden
if ($plannedHours > 0) {
print '';
print ''.img_picto('', 'clock', 'class="pictofixedwidth"').$langs->trans("PlannedHours").': ';
print ''.formatQty($plannedHours).' Std. '.$langs->trans("HoursFromOrder").' ';
print ' ';
}
print '
';
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 ''.img_picto('', 'note', 'class="pictofixedwidth"').$langs->trans("OrderDescription").' ';
print ' ';
print '';
print ''.dol_htmlentitiesbr($orderDescription).' ';
print ' ';
print '
';
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 '
';
foreach ($notesToShow as $note) {
$isChecked = ($note->checked == 1);
print '';
// Checkbox zum Abhaken
if ($notesStundenzettel->status == Stundenzettel::STATUS_DRAFT && $user->hasRight('stundenzettel', 'write')) {
$newChecked = $isChecked ? 0 : 1;
print '';
if ($isChecked) {
print ' ';
} else {
print ' ';
}
print ' ';
} else {
if ($isChecked) {
print ' ';
} else {
print ' ';
}
}
// Notiz-Text
print '';
print dol_escape_htmltag($note->note);
print ' ';
print ' ';
}
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 ''.$langs->trans("Ref").' ';
print ''.$langs->trans("Date").' ';
print ''.$langs->trans("Author").' ';
print ''.$langs->trans("Products").' ';
print ''.$langs->trans("LeistungDuration").' ';
print ''.$langs->trans("Status").' ';
print ' ';
if ($numStz > 0) {
while ($objStz = $db->fetch_object($resqlStzList)) {
$isCurrentStz = ($stundenzettel_id > 0 && $objStz->rowid == $stundenzettel_id);
print '';
// Ref
print '';
print '';
print img_picto('', 'clock', 'class="pictofixedwidth"').$objStz->ref;
print ' ';
if ($isCurrentStz) {
print ' aktuell ';
}
print ' ';
// Datum
print ''.dol_print_date($db->jdate($objStz->date_stundenzettel), 'day').' ';
// Autor
print ''.$objStz->firstname.' '.$objStz->lastname.' ';
// Gesamtmenge Produkte
print '';
if ($objStz->total_qty_products > 0) {
print formatQty($objStz->total_qty_products);
} else {
print '- ';
}
print ' ';
// Gesamtzeit Leistungen
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 ' ';
// Status
$stzTemp = new Stundenzettel($db);
print ''.$stzTemp->LibStatut($objStz->status, 5).' ';
print ' ';
}
} else {
print ''.$langs->trans("NoRecordFound").' ';
}
print '
';
print '
';
// Button: Neuen Stundenzettel erstellen
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 ''.$langs->trans("Filter").': ';
print '';
print ''.$langs->trans("FilterOpen").' ';
print ''.$langs->trans("FilterDone").' ';
print ''.$langs->trans("FilterAll").' ';
print ' ';
print '
';
// Alle ein-/ausklappen Buttons
print '
';
print '';
print ' '.$langs->trans("ExpandAll");
print ' ';
print ' ';
print '';
print ' '.$langs->trans("CollapseAll");
print ' ';
print '
';
print '
';
// JavaScript für Filter-Speicherung pro Auftrag
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 ' '.$langs->trans("ShowDetails");
print ' ';
print '';
print ' '.$langs->trans("HideDetails");
print ' ';
print '
';
print '
';
print '
';
print '';
print ''.$langs->trans("Product").' ';
print ''.$langs->trans("QtyOrdered").' ';
print ''.$langs->trans("QtyDelivered").' ';
print ''.$langs->trans("QtyRemaining").' ';
print ''.$langs->trans("Status").' ';
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 '';
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 ' ';
// Bestellt (mit Badges für Mehraufwand/Entfällt)
print '';
print ''.formatQty($effective_ordered).' ';
if ($qty_additional > 0) {
print ' +'.formatQty($qty_additional).' ';
}
if ($qty_omitted > 0) {
print ' -'.formatQty($qty_omitted).' ';
}
print ' ';
$total_ordered += $effective_ordered;
// Geliefert/Erfasst
print ''.formatQty($obj->qty_delivered).' ';
$total_delivered += $obj->qty_delivered;
// Verbleibend
print '';
if ($qty_remaining > 0) {
print ''.formatQty($qty_remaining).' ';
} elseif ($qty_remaining == 0) {
print '0 ';
} else {
print ''.formatQty($qty_remaining).' ';
}
print ' ';
$total_remaining += $qty_remaining;
// Status
print '';
if ($qty_remaining <= 0) {
print ''.$langs->trans("TrackingDone").' ';
} elseif ($obj->qty_delivered > 0) {
print ''.$langs->trans("TrackingPartial").' ';
} else {
print ''.$langs->trans("TrackingOpen").' ';
}
print ' ';
print ' ';
// Detail-Zeile (standardmäßig eingeklappt)
if ($hasDetails) {
print '';
print '';
// Sub-Tabelle für Details
print '';
print '';
print ''.$langs->trans("Stundenzettel").' ';
print ''.$langs->trans("Date").' ';
print ''.$langs->trans("Type").' ';
print ''.$langs->trans("Qty").' ';
print ''.$langs->trans("Reason").' ';
print ' ';
foreach ($details as $det) {
print '';
// Stundenzettel-Referenz mit Link
print '';
print '';
print $det->stz_ref;
print ' ';
print ' ';
// Datum
print ''.dol_print_date($db->jdate($det->date_stundenzettel), 'day').' ';
// Typ (origin)
print '';
switch ($det->origin) {
case 'order':
case 'added':
print ''.$langs->trans("QtyDelivered").' ';
break;
case 'additional':
print ''.$langs->trans("Mehraufwand").' ';
break;
case 'omitted':
print ''.$langs->trans("Entfaellt").' ';
break;
default:
print $det->origin;
}
print ' ';
// Menge
print ''.formatQty($det->qty_done).' ';
// Grund/Beschreibung
print '';
if (!empty($det->description)) {
print dol_escape_htmltag($det->description);
} else {
print '- ';
}
print ' ';
print ' ';
}
print '
';
print ' ';
print ' ';
}
}
// Summenzeile
print '';
print ''.$langs->trans("Total").' ';
print ''.formatQty($total_ordered).' ';
print ''.formatQty($total_delivered).' ';
print ''.formatQty($total_remaining).' ';
print ' ';
print ' ';
} else {
print ''.$langs->trans("NoRecordFound").' ';
}
}
// =============================================
// 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 ''.$langs->trans("Mehraufwand").' ('.$langs->trans("MehraufwandDesc").') ';
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 '';
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 ' ';
// Bestellt (Mehraufwand-Menge)
print ''.formatQty($qty_ordered_mehr).' ';
$total_ordered += $qty_ordered_mehr;
// Geliefert
print ''.formatQty($qty_delivered_mehr).' ';
$total_delivered += $qty_delivered_mehr;
// Verbleibend
print '';
if ($qty_remaining_mehr > 0) {
print ''.formatQty($qty_remaining_mehr).' ';
} elseif ($qty_remaining_mehr == 0) {
print '0 ';
} else {
print ''.formatQty($qty_remaining_mehr).' ';
}
print ' ';
$total_remaining += $qty_remaining_mehr;
// Status
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 ' ';
print ' ';
// Detail-Zeile für Mehraufwand
if ($hasDetailsMehr) {
print '';
print '';
print '';
print '';
print ''.$langs->trans("Stundenzettel").' ';
print ''.$langs->trans("Date").' ';
print ''.$langs->trans("Type").' ';
print ''.$langs->trans("Qty").' ';
print ''.$langs->trans("Reason").' ';
print ' ';
foreach ($detailsMehr as $det) {
print '';
print ''.$det->stz_ref.' ';
print ''.dol_print_date($db->jdate($det->date_stundenzettel), 'day').' ';
print ''.$langs->trans("Mehraufwand").' ';
print ''.formatQty($det->qty_done).' ';
print ''.((!empty($det->description)) ? dol_escape_htmltag($det->description) : '- ').' ';
print ' ';
}
print '
';
print ' ';
print ' ';
}
}
// Neue Summenzeile mit Mehraufwand
print '';
print ''.$langs->trans("Total").' ('.$langs->trans("Mehraufwand").' '.$langs->trans("incl").') ';
print ''.formatQty($total_ordered).' ';
print ''.formatQty($total_delivered).' ';
print ''.formatQty($total_remaining).' ';
print ' ';
print ' ';
}
print '
';
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 ''.$langs->trans("DefaultService").' ';
print ''.$langs->trans("TotalHours").' ';
print ''.$langs->trans("Entries").' ';
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 '';
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 ' ';
// Stunden
print ''.sprintf('%d:%02d h', $hours, $mins).' ';
// Anzahl Einträge
print ''.$objL->entry_count.' ';
print ' ';
}
// Summenzeile
$totalHours = floor($totalMinutesAll / 60);
$totalMins = $totalMinutesAll % 60;
print '';
print ''.$langs->trans("Total").' ';
print ''.sprintf('%d:%02d h', $totalHours, $totalMins).' ';
print ' ';
print ' ';
} else {
print ''.$langs->trans("NoRecordFound").' ';
}
}
print '
';
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 ''.$langs->trans("Stundenzettel").' ';
print ''.$langs->trans("Date").' ';
print ''.$langs->trans("LeistungTimeStart").' - '.$langs->trans("LeistungTimeEnd").' ';
print ''.$langs->trans("LeistungDuration").' ';
print ''.$langs->trans("DefaultService").' ';
print ''.$langs->trans("Description").' ';
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 '';
print '';
print $objLD->stz_ref;
print ' ';
print ' ';
// Datum
print ''.dol_print_date($db->jdate($objLD->date_leistung), 'day').' ';
// Zeit
print '';
if ($objLD->time_start && $objLD->time_end) {
print substr($objLD->time_start, 0, 5).' - '.substr($objLD->time_end, 0, 5);
} else {
print '- ';
}
print ' ';
// Dauer
print ''.sprintf('%d:%02d h', $hours, $mins).' ';
// Leistungsposition
print '';
if ($objLD->fk_product > 0) {
print ''.$objLD->service_ref.' ';
} else {
print '- ';
}
print ' ';
// Beschreibung
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 ' ';
print ' ';
}
} else {
print ''.$langs->trans("NoRecordFound").' ';
}
}
print '
';
print '
';
print '
'; // fichehalfleft
// Button: In Rechnung übertragen (nur wenn alles erledigt)
if ($total_remaining <= 0 && $total_delivered > 0) {
print '
';
}
}
print '
'; // fichecenter
dol_fiche_end();
llxFooter();
$db->close();