2325 lines
111 KiB
PHP
2325 lines
111 KiB
PHP
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* 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');
|
|
|
|
// 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);
|
|
|
|
$productResult = $facture->addline(
|
|
$product->label, // 1: desc
|
|
$product->price, // 2: pu_ht
|
|
$addProd->total_qty, // 3: qty
|
|
$product->tva_tx, // 4: txtva
|
|
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
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
dol_include_once('/stundenzettel/lib/stundenzettel.lib.php');
|
|
$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');
|
|
|
|
// Tabs - Stundenzettel-Tabs wenn aus Stundenzettel aufgerufen, sonst Auftrags-Tabs
|
|
// Aktiven Tab-Key basierend auf $tab bestimmen
|
|
$activeTabKey = 'productlist'; // Default
|
|
if ($tab == 'stundenzettel') {
|
|
$activeTabKey = 'stundenzettel_list';
|
|
} elseif ($tab == 'tracking') {
|
|
$activeTabKey = 'tracking';
|
|
}
|
|
|
|
if ($stundenzettelObj) {
|
|
$head = stundenzettel_prepare_head($stundenzettelObj);
|
|
dol_fiche_head($head, $activeTabKey, $langs->trans("Stundenzettel"), -1, 'clock');
|
|
} else {
|
|
$head = commande_prepare_head($order);
|
|
dol_fiche_head($head, 'stundenzettel', $langs->trans("CustomerOrder"), -1, 'order');
|
|
}
|
|
|
|
$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 = '<div class="warning" style="margin-top:10px; padding:10px;">';
|
|
$remainingHtml .= '<strong>'.$langs->trans("RemainingProductsWarning").':</strong><ul style="margin:10px 0;">';
|
|
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 .= '<li>'.$productName.' - <strong>'.formatQty($qtyRemaining).'</strong> '.$langs->trans("QtyRemaining").'</li>';
|
|
}
|
|
$remainingHtml .= '</ul></div>';
|
|
$confirmMessage = $langs->trans('ConfirmReleaseWithRemaining').$remainingHtml.'<br><br><strong>'.$langs->trans('ConfirmReleaseFinal').'</strong>';
|
|
} 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 = '<a href="'.DOL_URL_ROOT.'/commande/list.php?restore_lastsearch_values=1">'.$langs->trans("BackToList").'</a>';
|
|
dol_banner_tab($order, 'ref', $linkback, 1, 'ref', 'ref');
|
|
} elseif ($stundenzettelObj) {
|
|
$linkback = '<a href="'.dol_buildpath('/stundenzettel/list.php', 1).'?restore_lastsearch_values=1">'.$langs->trans("BackToList").'</a>';
|
|
dol_banner_tab($stundenzettelObj, 'id', $linkback, 1, 'rowid', 'ref');
|
|
} else {
|
|
$linkback = '<a href="'.DOL_URL_ROOT.'/commande/list.php?restore_lastsearch_values=1">'.$langs->trans("BackToList").'</a>';
|
|
dol_banner_tab($order, 'ref', $linkback, 1, 'ref', 'ref');
|
|
}
|
|
|
|
print '<div class="fichecenter">';
|
|
print '<div class="underbanner clearboth"></div>';
|
|
|
|
// 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 '<div class="info" style="margin-bottom:15px;">';
|
|
print '<table class="noborder" style="width:auto;">';
|
|
|
|
// Standard-Leistung
|
|
print '<tr>';
|
|
print '<td style="padding-right:20px;"><strong>'.img_picto('', 'service', 'class="pictofixedwidth"').$langs->trans("DefaultService").':</strong></td>';
|
|
print '<td>';
|
|
if ($defaultServiceProduct) {
|
|
print $defaultServiceProduct->getNomUrl(1).' - '.$defaultServiceProduct->label;
|
|
print ' <span class="opacitymedium">('.price($defaultServiceProduct->price, 0, $langs, 1, -1, -1, $conf->currency).'/Std.)</span>';
|
|
} else {
|
|
print '<span class="opacitymedium">'.$langs->trans("NoDefaultServiceSet").'</span>';
|
|
print ' <a href="'.DOL_URL_ROOT.'/societe/card.php?socid='.$order->socid.'" class="button small">'.$langs->trans("SetDefaultServiceInCustomer").'</a>';
|
|
}
|
|
print '</td>';
|
|
print '</tr>';
|
|
|
|
// Geplante Stunden
|
|
if ($plannedHours > 0) {
|
|
print '<tr>';
|
|
print '<td style="padding-right:20px;"><strong>'.img_picto('', 'clock', 'class="pictofixedwidth"').$langs->trans("PlannedHours").':</strong></td>';
|
|
print '<td><span class="badge badge-info">'.formatQty($plannedHours).' Std.</span> '.$langs->trans("HoursFromOrder").'</td>';
|
|
print '</tr>';
|
|
}
|
|
|
|
print '</table>';
|
|
print '</div>';
|
|
}
|
|
|
|
// 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 '<div class="fichehalfleft" style="margin-bottom: 15px;">';
|
|
print '<div class="div-table-responsive-no-min">';
|
|
print '<table class="noborder centpercent">';
|
|
print '<tr class="liste_titre">';
|
|
print '<th>'.img_picto('', 'note', 'class="pictofixedwidth"').$langs->trans("OrderDescription").'</th>';
|
|
print '</tr>';
|
|
print '<tr class="oddeven">';
|
|
print '<td>'.dol_htmlentitiesbr($orderDescription).'</td>';
|
|
print '</tr>';
|
|
print '</table>';
|
|
print '</div>';
|
|
print '</div>';
|
|
print '<div class="clearboth"></div>';
|
|
}
|
|
|
|
// 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 '<div class="info" style="margin-bottom:20px;">';
|
|
print '<strong>'.img_picto('', 'note', 'class="pictofixedwidth"').$langs->trans("NotesForNextVisit").':</strong><br>';
|
|
while ($obj = $db->fetch_object($resql)) {
|
|
print '<div style="margin:5px 0; padding:5px; background:#fff; border-left:3px solid #0077b3;">';
|
|
print '<small class="opacitymedium">'.dol_print_date($db->jdate($obj->date_stundenzettel), 'day').' ('.$obj->ref.'):</small><br>';
|
|
print dol_htmlentitiesbr($obj->note_public);
|
|
print '</div>';
|
|
}
|
|
print '</div>';
|
|
}
|
|
|
|
// 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 '<div class="info" style="margin-bottom:15px; background-color: #fff8e1; border-left: 4px solid #ffc107; padding: 10px;">';
|
|
print '<strong>'.img_picto('', 'list', 'class="pictofixedwidth"').$langs->trans("NotesMemo").'</strong>';
|
|
print ' <small class="opacitymedium">('.$notesStundenzettel->ref.')</small>';
|
|
print '<ul style="margin: 5px 0 0 0; padding-left: 20px; list-style: none;">';
|
|
|
|
foreach ($notesToShow as $note) {
|
|
$isChecked = ($note->checked == 1);
|
|
print '<li style="margin: 5px 0; display: flex; align-items: flex-start; gap: 8px;">';
|
|
|
|
// Checkbox zum Abhaken
|
|
if ($notesStundenzettel->status == Stundenzettel::STATUS_DRAFT && $user->hasRight('stundenzettel', 'write')) {
|
|
$newChecked = $isChecked ? 0 : 1;
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$order->id.'&action=toggle_note¬e_id='.$note->rowid.'&checked='.$newChecked.'&stundenzettel_id='.$notesStundenzettel->id.'&token='.newToken().'" style="text-decoration: none;">';
|
|
if ($isChecked) {
|
|
print '<span class="fas fa-check-square" style="color: #28a745; font-size: 1.1em;"></span>';
|
|
} else {
|
|
print '<span class="far fa-square" style="color: #6c757d; font-size: 1.1em;"></span>';
|
|
}
|
|
print '</a>';
|
|
} else {
|
|
if ($isChecked) {
|
|
print '<span class="fas fa-check-square" style="color: #28a745; font-size: 1.1em;"></span>';
|
|
} else {
|
|
print '<span class="far fa-square" style="color: #6c757d; font-size: 1.1em;"></span>';
|
|
}
|
|
}
|
|
|
|
// Notiz-Text
|
|
print '<span'.($isChecked ? ' style="text-decoration: line-through; color: #6c757d;"' : '').'>';
|
|
print dol_escape_htmltag($note->note);
|
|
print '</span>';
|
|
|
|
print '</li>';
|
|
}
|
|
|
|
print '</ul>';
|
|
print '</div>';
|
|
}
|
|
|
|
// =============================================
|
|
// 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 '<div class="div-table-responsive-no-min">';
|
|
print '<table class="noborder centpercent">';
|
|
print '<tr class="liste_titre">';
|
|
print '<th>'.$langs->trans("Ref").'</th>';
|
|
print '<th>'.$langs->trans("Date").'</th>';
|
|
print '<th>'.$langs->trans("Author").'</th>';
|
|
print '<th class="right">'.$langs->trans("Products").'</th>';
|
|
print '<th class="right">'.$langs->trans("LeistungDuration").'</th>';
|
|
print '<th>'.$langs->trans("Status").'</th>';
|
|
print '</tr>';
|
|
|
|
if ($numStz > 0) {
|
|
while ($objStz = $db->fetch_object($resqlStzList)) {
|
|
$isCurrentStz = ($stundenzettel_id > 0 && $objStz->rowid == $stundenzettel_id);
|
|
print '<tr class="oddeven"'.($isCurrentStz ? ' style="background-color: #e8f4fc;"' : '').'>';
|
|
|
|
// Ref
|
|
print '<td>';
|
|
print '<a href="'.dol_buildpath('/stundenzettel/card.php?id='.$objStz->rowid, 1).'">';
|
|
print img_picto('', 'clock', 'class="pictofixedwidth"').$objStz->ref;
|
|
print '</a>';
|
|
if ($isCurrentStz) {
|
|
print ' <span class="badge badge-info">aktuell</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Datum
|
|
print '<td>'.dol_print_date($db->jdate($objStz->date_stundenzettel), 'day').'</td>';
|
|
|
|
// Autor
|
|
print '<td>'.$objStz->firstname.' '.$objStz->lastname.'</td>';
|
|
|
|
// Gesamtmenge Produkte
|
|
print '<td class="right">';
|
|
if ($objStz->total_qty_products > 0) {
|
|
print formatQty($objStz->total_qty_products);
|
|
} else {
|
|
print '<span class="opacitymedium">-</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Gesamtzeit Leistungen
|
|
print '<td class="right">';
|
|
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 '<span class="opacitymedium">-</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Status
|
|
$stzTemp = new Stundenzettel($db);
|
|
print '<td>'.$stzTemp->LibStatut($objStz->status, 5).'</td>';
|
|
|
|
print '</tr>';
|
|
}
|
|
} else {
|
|
print '<tr class="oddeven"><td colspan="6" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
|
|
}
|
|
|
|
print '</table>';
|
|
print '</div>';
|
|
|
|
// Button: Neuen Stundenzettel erstellen
|
|
print '<div class="center" style="margin-top: 20px;">';
|
|
print '<a class="butAction" href="'.dol_buildpath('/stundenzettel/card.php?action=create&fk_commande='.$orderIdForQuery, 1).'">';
|
|
print img_picto('', 'add', 'class="pictofixedwidth"').$langs->trans("CreateStundenzettel");
|
|
print '</a>';
|
|
print '</div>';
|
|
}
|
|
|
|
// =============================================
|
|
// 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 '<div class="marginbottomonly" style="display: flex; align-items: center; gap: 20px; flex-wrap: wrap;">';
|
|
|
|
// Filter
|
|
print '<div>';
|
|
print '<label for="filter" style="margin-right: 5px;">'.$langs->trans("Filter").':</label>';
|
|
print '<select name="filter" id="filter" class="flat" onchange="saveFilterAndRedirect(this.value)">';
|
|
print '<option value="open"'.($filter == 'open' ? ' selected' : '').'>'.$langs->trans("FilterOpen").'</option>';
|
|
print '<option value="done"'.($filter == 'done' ? ' selected' : '').'>'.$langs->trans("FilterDone").'</option>';
|
|
print '<option value="all"'.($filter == 'all' ? ' selected' : '').'>'.$langs->trans("FilterAll").'</option>';
|
|
print '</select>';
|
|
print '</div>';
|
|
|
|
// Alle ein-/ausklappen Buttons
|
|
print '<div>';
|
|
print '<button type="button" class="button small" onclick="expandAllSections()" title="'.$langs->trans("ExpandAll").'">';
|
|
print '<span class="fas fa-expand-arrows-alt"></span> '.$langs->trans("ExpandAll");
|
|
print '</button>';
|
|
print ' ';
|
|
print '<button type="button" class="button small" onclick="collapseAllSections()" title="'.$langs->trans("CollapseAll").'">';
|
|
print '<span class="fas fa-compress-arrows-alt"></span> '.$langs->trans("CollapseAll");
|
|
print '</button>';
|
|
print '</div>';
|
|
|
|
print '</div>';
|
|
|
|
// JavaScript für Filter-Speicherung pro Auftrag
|
|
print '<script>
|
|
var orderId = '.$order->id.';
|
|
var stundenzettelId = '.($stundenzettel_id > 0 ? $stundenzettel_id : 0).';
|
|
|
|
// Beim Laden: Filter aus localStorage holen (falls nicht per URL gesetzt)
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
var urlParams = new URLSearchParams(window.location.search);
|
|
// WICHTIG: Nicht umleiten wenn eine action aktiv ist (z.B. Bestätigungsdialog)
|
|
if (urlParams.has("action")) {
|
|
return; // Bei Actions keine Filter-Weiterleitung
|
|
}
|
|
if (!urlParams.has("filter")) {
|
|
var savedFilter = localStorage.getItem("stz_filter_" + orderId);
|
|
if (savedFilter && savedFilter !== "'.$filter.'") {
|
|
// Redirect mit gespeichertem Filter
|
|
saveFilterAndRedirect(savedFilter);
|
|
}
|
|
}
|
|
});
|
|
|
|
function saveFilterAndRedirect(filterValue) {
|
|
// Filter in localStorage speichern
|
|
localStorage.setItem("stz_filter_" + orderId, filterValue);
|
|
// Redirect (noredirect beibehalten um Auto-Redirect zu verhindern)
|
|
var url = "?id=" + orderId + "&tab=products&filter=" + filterValue + "&noredirect=1";
|
|
if (stundenzettelId > 0) {
|
|
url += "&stundenzettel_id=" + stundenzettelId;
|
|
}
|
|
window.location.href = url;
|
|
}
|
|
</script>';
|
|
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$order->id.'">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="transfer_products">';
|
|
if ($stundenzettel_id > 0) {
|
|
print '<input type="hidden" name="stundenzettel_id" value="'.$stundenzettel_id.'">';
|
|
}
|
|
|
|
// 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 '<div class="marginbottomonly">';
|
|
print '<label for="date_stundenzettel">'.$langs->trans("StundenzettelDate").':</label> ';
|
|
print '<input type="date" name="date_stundenzettel" id="date_stundenzettel" value="'.$defaultDate.'" class="flat">';
|
|
print '</div>';
|
|
} 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 '<div class="marginbottomonly">';
|
|
if ($stzDateStr == $todayStr) {
|
|
// Stundenzettel ist von heute - normal anzeigen
|
|
print '<span class="opacitymedium">'.$langs->trans("TransferToStundenzettel").': </span>';
|
|
print '<strong>'.$stundenzettelObj->ref.'</strong> ('.dol_print_date($stundenzettelObj->date_stundenzettel, 'day').')';
|
|
} else {
|
|
// Stundenzettel ist von einem anderen Tag - Hinweis dass neuer erstellt wird
|
|
print '<span class="opacitymedium">'.$langs->trans("TransferToStundenzettel").': </span>';
|
|
print '<strong style="color: #28a745;">'.$langs->trans("NewStundenzettelForToday").'</strong>';
|
|
print ' <span class="opacitymedium">('.dol_print_date(dol_now(), 'day').')</span>';
|
|
print '<br><small class="opacitymedium">'.$langs->trans("SelectedStundenzettelNotToday", $stundenzettelObj->ref, dol_print_date($stundenzettelObj->date_stundenzettel, 'day')).'</small>';
|
|
// Hidden field für das heutige Datum
|
|
print '<input type="hidden" name="force_new_stundenzettel" value="1">';
|
|
print '<input type="hidden" name="date_stundenzettel" value="'.date('Y-m-d').'">';
|
|
}
|
|
print '</div>';
|
|
}
|
|
|
|
print '<div class="div-table-responsive-no-min">';
|
|
print '<table class="noborder centpercent">';
|
|
print '<tr class="liste_titre">';
|
|
print '<th class="center" style="width:30px;"><input type="checkbox" id="checkall" onclick="toggleAll(this)"></th>';
|
|
print '<th>'.$langs->trans("Product").'</th>';
|
|
print '<th class="right">'.$langs->trans("QtyOrdered").'</th>';
|
|
print '<th class="right">'.$langs->trans("QtyDelivered").'</th>';
|
|
print '<th class="right">'.$langs->trans("QtyRemaining").'</th>';
|
|
print '<th>'.$langs->trans("Status").'</th>';
|
|
print '</tr>';
|
|
|
|
// 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 '<tr class="oddeven'.$sectionClass.'">';
|
|
|
|
// Checkbox (nur bei offenen anzeigen UND wenn noch nicht auf diesem Stundenzettel)
|
|
$isAlreadyOnStz = isset($alreadyOnStundenzettel[$obj->rowid]);
|
|
print '<td class="center"'.$styleFirst.'>';
|
|
if (!$isDone && !$isAlreadyOnStz) {
|
|
print '<input type="checkbox" name="selected[]" value="'.$obj->rowid.'" class="product-checkbox">';
|
|
} elseif ($isAlreadyOnStz) {
|
|
print '<span class="fas fa-check" style="color: #28a745;" title="'.$langs->trans("AlreadyOnStundenzettel").'"></span>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Produkt
|
|
print '<td>';
|
|
if ($obj->fk_product > 0) {
|
|
print '<a href="'.DOL_URL_ROOT.'/product/card.php?id='.$obj->fk_product.'">';
|
|
print img_picto('', 'product', 'class="pictofixedwidth"');
|
|
print $obj->product_ref.' - '.$obj->product_label;
|
|
print '</a>';
|
|
} 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 '<span class="opacitymedium">'.$desc.'</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Menge bestellt - zeigt Gesamtzahl mit Info über Änderungen
|
|
print '<td class="right">';
|
|
// Gesamtzahl (effektiv) als Hauptzahl
|
|
print '<strong>'.formatQty($effectiveTotal).'</strong>';
|
|
// Änderungen als kleine Info-Badges
|
|
if ($qty_added > 0) {
|
|
print ' <span class="badge" style="background-color: #28a745; color: #fff; font-size: 0.75em;" title="'.$langs->trans("AdditionalQty").'">+'.formatQty($qty_added).'</span>';
|
|
}
|
|
if ($qty_removed > 0) {
|
|
print ' <span class="badge" style="background-color: #dc3545; color: #fff; font-size: 0.75em;" title="'.$langs->trans("Entfaellt").'">-'.formatQty($qty_removed).'</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Menge geliefert/verbaut
|
|
print '<td class="right">'.formatQty($obj->qty_delivered).'</td>';
|
|
|
|
// Verbleibend
|
|
print '<td class="right">';
|
|
if ($remaining > 0) {
|
|
print '<span class="badge badge-warning">'.formatQty($remaining).'</span>';
|
|
} elseif ($remaining == 0) {
|
|
print '<span class="badge badge-success">0</span>';
|
|
} else {
|
|
// Mehr verbaut als bestellt
|
|
print '<span class="badge badge-info">'.formatQty($remaining).'</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Status
|
|
print '<td>';
|
|
if ($isDone) {
|
|
print '<span class="badge badge-success">'.$langs->trans("TrackingDone").'</span>';
|
|
} elseif ($obj->qty_delivered > 0) {
|
|
print '<span class="badge badge-warning">'.$langs->trans("TrackingPartial").'</span>';
|
|
} else {
|
|
print '<span class="badge badge-secondary">'.$langs->trans("TrackingOpen").'</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
print '</tr>';
|
|
|
|
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 = '<tr class="liste_titre section-header" data-section="section_'.$sectionId.'" onclick="toggleSection(\'section_'.$sectionId.'\')" style="cursor: pointer;">';
|
|
$sectionHeader .= '<td colspan="6" style="font-weight: bold; padding: 8px; border-left: 4px solid '.$section['color'].'; background-color: #f8f8f8;">';
|
|
$sectionHeader .= '<span class="section-toggle" id="toggle_section_'.$sectionId.'" style="margin-right: 10px;">▼</span>';
|
|
$sectionHeader .= dol_escape_htmltag($section['title']);
|
|
$sectionHeader .= ' <span style="opacity: 0.6; font-weight: normal;">('.$visibleProductsInSection.' '.$langs->trans("Products").')</span>';
|
|
$sectionHeader .= '</td>';
|
|
$sectionHeader .= '</tr>';
|
|
|
|
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 '<tr class="liste_titre section-header" data-section="section_other" onclick="toggleSection(\'section_other\')" style="cursor: pointer;">';
|
|
print '<td colspan="6" style="font-weight: bold; padding: 8px; border-left: 4px solid #666; background-color: #f8f8f8;">';
|
|
print '<span class="section-toggle" id="toggle_section_other" style="margin-right: 10px;">▼</span>';
|
|
print $langs->trans("OtherProducts").' <span style="opacity: 0.6; font-weight: normal;">('.$visibleProductsWithoutSection.' '.$langs->trans("Products").')</span>';
|
|
print '</td>';
|
|
print '</tr>';
|
|
}
|
|
|
|
foreach ($products_without_section as $prod) {
|
|
if ($printProductRow($prod, null, 'other')) {
|
|
$totalProducts++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($totalProducts == 0) {
|
|
print '<tr class="oddeven"><td colspan="6" class="opacitymedium">'.$langs->trans("AllProductsDocumented").'</td></tr>';
|
|
}
|
|
|
|
// =============================================
|
|
// 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 '<tr class="liste_titre section-header" data-section="section_mehraufwand" onclick="toggleSection(\'section_mehraufwand\')" style="cursor: pointer;">';
|
|
print '<td colspan="6" style="font-weight: bold; padding: 8px; border-left: 4px solid #e67e22; background-color: #f8f8f8;">';
|
|
print '<span class="section-toggle" id="toggle_section_mehraufwand" style="margin-right: 10px;">▼</span>';
|
|
print '<span style="margin-right: 10px; color: #e67e22;">⚠</span>';
|
|
print $langs->trans("Mehraufwand");
|
|
print ' <span style="opacity: 0.6; font-weight: normal;">('.$mehraufwandCount.' '.$langs->trans("Products").')</span>';
|
|
print '</td>';
|
|
print '</tr>';
|
|
|
|
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 '<tr class="oddeven section-product section_mehraufwand">';
|
|
|
|
// Checkbox - Mehraufwand immer anzeigen (für Auswahl/Übersicht)
|
|
print '<td class="center">';
|
|
if ($productIds) {
|
|
print '<input type="checkbox" name="selected_mehraufwand[]" value="'.$productIds.'" class="product-checkbox">';
|
|
}
|
|
print '</td>';
|
|
|
|
// Produkt
|
|
print '<td>';
|
|
if ($objMa->fk_product > 0) {
|
|
print '<a href="'.DOL_URL_ROOT.'/product/card.php?id='.$objMa->fk_product.'">';
|
|
print img_picto('', 'product', 'class="pictofixedwidth"');
|
|
print $objMa->product_ref.' - '.$objMa->product_label;
|
|
print '</a>';
|
|
} 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 '<span class="opacitymedium">'.$desc.'</span>';
|
|
}
|
|
print ' <span class="badge badge-warning">'.$langs->trans("Mehraufwand").'</span>';
|
|
print '</td>';
|
|
|
|
// Menge beauftragt (Zielmenge - was verbaut werden soll)
|
|
print '<td class="right">'.formatQty($qtyTarget).'</td>';
|
|
|
|
// Menge tatsächlich verbaut
|
|
print '<td class="right">'.formatQty($qtyDone).'</td>';
|
|
|
|
// Verbleibend (Zielmenge - Verbaut)
|
|
print '<td class="right">';
|
|
if ($qtyRemaining > 0) {
|
|
print '<span class="badge badge-warning">'.formatQty($qtyRemaining).'</span>';
|
|
} elseif ($qtyRemaining < 0) {
|
|
// Mehr verbaut als geplant
|
|
print '<span class="badge badge-info">'.formatQty($qtyRemaining).'</span>';
|
|
} else {
|
|
print '<span class="badge badge-success">0</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Status
|
|
print '<td>';
|
|
if ($qtyDone > 0 && $qtyRemaining > 0) {
|
|
print '<span class="badge badge-warning">'.$langs->trans("TrackingPartial").'</span>';
|
|
} elseif ($qtyDone == 0) {
|
|
print '<span class="badge badge-secondary">'.$langs->trans("TrackingOpen").'</span>';
|
|
} else {
|
|
print '<span class="badge badge-success">'.$langs->trans("TrackingDone").'</span>';
|
|
}
|
|
print ' <small class="opacitymedium" title="'.$objMa->stundenzettel_refs.'">('.$langs->trans("FromStundenzettel").')</small>';
|
|
print '</td>';
|
|
|
|
print '</tr>';
|
|
}
|
|
}
|
|
|
|
// 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 '</table>';
|
|
print '</div>';
|
|
|
|
// Button nur anzeigen wenn nicht freigegeben
|
|
if ($stundenzettelStatus == 0) {
|
|
print '<div class="center" style="margin-top:20px;">';
|
|
print '<input type="submit" class="button button-primary" value="'.$langs->trans("TransferProducts").'">';
|
|
print '</div>';
|
|
} elseif ($stundenzettelStatus == 1) {
|
|
// Status 1: Freigegeben - zeige Meldung mit Wiedereröffnen-Button
|
|
print '<div class="warning" style="margin-top:20px;">';
|
|
print img_picto('', 'lock', 'class="pictofixedwidth"');
|
|
print $langs->trans("StundenzettelReleasedNoChanges");
|
|
print ' <a class="button small" href="'.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1&action=reopen_stundenzettel" style="margin-left:10px;">';
|
|
print img_picto('', 'unlock', 'class="pictofixedwidth"').$langs->trans("ReopenStundenzettel");
|
|
print '</a>';
|
|
print '</div>';
|
|
} else {
|
|
// Status 2: In Rechnung übertragen
|
|
print '<div class="info" style="margin-top:20px;">';
|
|
print img_picto('', 'check', 'class="pictofixedwidth"');
|
|
print $langs->trans("StundenzettelTransferredToInvoice");
|
|
print ' <a class="button small" href="'.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1&action=reset_stundenzettel" style="margin-left:10px;">';
|
|
print img_picto('', 'undo', 'class="pictofixedwidth"').$langs->trans("ResetStundenzettel");
|
|
print '</a>';
|
|
print '</div>';
|
|
}
|
|
|
|
print '</form>';
|
|
|
|
// JavaScript für "Alle auswählen" und Section-Toggle
|
|
print '<script>
|
|
function toggleAll(source) {
|
|
var checkboxes = document.getElementsByClassName("product-checkbox");
|
|
for (var i = 0; i < checkboxes.length; i++) {
|
|
checkboxes[i].checked = source.checked;
|
|
}
|
|
}
|
|
|
|
// Section ein-/ausklappen
|
|
function toggleSection(sectionId) {
|
|
var rows = document.querySelectorAll("tr." + sectionId);
|
|
var toggle = document.getElementById("toggle_" + sectionId);
|
|
var isCollapsed = toggle.textContent.trim() === "▶";
|
|
|
|
for (var i = 0; i < rows.length; i++) {
|
|
rows[i].style.display = isCollapsed ? "" : "none";
|
|
}
|
|
toggle.textContent = isCollapsed ? "▼" : "▶";
|
|
|
|
// Zustand in localStorage speichern
|
|
saveSectionState(sectionId, !isCollapsed);
|
|
}
|
|
|
|
// Alle Sections ausklappen
|
|
function expandAllSections() {
|
|
var headers = document.querySelectorAll(".section-header");
|
|
headers.forEach(function(header) {
|
|
var sectionId = header.dataset.section;
|
|
var rows = document.querySelectorAll("tr." + sectionId);
|
|
var toggle = document.getElementById("toggle_" + sectionId);
|
|
|
|
for (var i = 0; i < rows.length; i++) {
|
|
rows[i].style.display = "";
|
|
}
|
|
if (toggle) toggle.textContent = "▼";
|
|
saveSectionState(sectionId, false);
|
|
});
|
|
}
|
|
|
|
// Alle Sections einklappen
|
|
function collapseAllSections() {
|
|
var headers = document.querySelectorAll(".section-header");
|
|
headers.forEach(function(header) {
|
|
var sectionId = header.dataset.section;
|
|
var rows = document.querySelectorAll("tr." + sectionId);
|
|
var toggle = document.getElementById("toggle_" + sectionId);
|
|
|
|
for (var i = 0; i < rows.length; i++) {
|
|
rows[i].style.display = "none";
|
|
}
|
|
if (toggle) toggle.textContent = "▶";
|
|
saveSectionState(sectionId, true);
|
|
});
|
|
}
|
|
|
|
// Zustand in localStorage speichern
|
|
function saveSectionState(sectionId, collapsed) {
|
|
var states = JSON.parse(localStorage.getItem("stz_sections_" + orderId) || "{}");
|
|
states[sectionId] = collapsed;
|
|
localStorage.setItem("stz_sections_" + orderId, JSON.stringify(states));
|
|
}
|
|
|
|
// Zustand beim Laden wiederherstellen
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
var states = JSON.parse(localStorage.getItem("stz_sections_" + orderId) || "{}");
|
|
for (var sectionId in states) {
|
|
if (states[sectionId]) {
|
|
var rows = document.querySelectorAll("tr." + sectionId);
|
|
var toggle = document.getElementById("toggle_" + sectionId);
|
|
|
|
for (var i = 0; i < rows.length; i++) {
|
|
rows[i].style.display = "none";
|
|
}
|
|
if (toggle) toggle.textContent = "▶";
|
|
}
|
|
}
|
|
});
|
|
</script>';
|
|
|
|
// =============================================
|
|
// AKTIONSBUTTONS FÜR STUNDENZETTEL-FREIGABE
|
|
// =============================================
|
|
if ($stundenzettelStatus < 2) {
|
|
print '<div class="tabsAction" style="margin-top: 20px;">';
|
|
|
|
if ($stundenzettelStatus == 0) {
|
|
// Status: Offen - Zeige Freigeben-Button wenn Stundenzettel vorhanden
|
|
if ($hasStundenzettel && $allValidated) {
|
|
if ($hasRemainingProducts) {
|
|
// Mit Warnung
|
|
print '<a class="butActionDelete" href="'.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1&action=confirm_release_warning">';
|
|
print img_picto('', 'lock', 'class="pictofixedwidth"').$langs->trans("ReleaseStundenzettel");
|
|
print '</a>';
|
|
} else {
|
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1&action=release_stundenzettel">';
|
|
print img_picto('', 'lock', 'class="pictofixedwidth"').$langs->trans("ReleaseStundenzettel");
|
|
print '</a>';
|
|
}
|
|
} elseif ($hasStundenzettel && !$allValidated) {
|
|
print '<span class="butActionRefused" title="'.$langs->trans("AllStundenzettelMustBeValidated").'">';
|
|
print img_picto('', 'lock', 'class="pictofixedwidth"').$langs->trans("ReleaseStundenzettel");
|
|
print '</span>';
|
|
}
|
|
} elseif ($stundenzettelStatus == 1) {
|
|
// Status: Freigegeben - Zeige Wiedereröffnen und Rechnung-Button
|
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1&action=reopen_stundenzettel">';
|
|
print img_picto('', 'unlock', 'class="pictofixedwidth"').$langs->trans("ReopenStundenzettel");
|
|
print '</a>';
|
|
|
|
if ($user->hasRight('facture', 'creer')) {
|
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1&action=transfer_invoice">';
|
|
print img_picto('', 'bill', 'class="pictofixedwidth"').$langs->trans("TransferToInvoice");
|
|
print '</a>';
|
|
}
|
|
}
|
|
|
|
print '</div>';
|
|
}
|
|
}
|
|
|
|
// =============================================
|
|
// 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 = $objD->fk_commandedet > 0 ? $objD->fk_commandedet : 'prod_'.$objD->fk_product;
|
|
if (!isset($trackingDetails[$key])) {
|
|
$trackingDetails[$key] = array();
|
|
}
|
|
$trackingDetails[$key][] = $objD;
|
|
}
|
|
}
|
|
|
|
// Buttons für Details anzeigen/verbergen
|
|
print '<div class="marginbottomonly" style="display:flex; justify-content:flex-end; gap:10px;">';
|
|
print '<button type="button" class="button small" onclick="expandAllTrackingDetails()" title="'.$langs->trans("ShowDetails").'">';
|
|
print '<span class="fas fa-list"></span> '.$langs->trans("ShowDetails");
|
|
print '</button>';
|
|
print '<button type="button" class="button small" onclick="collapseAllTrackingDetails()" title="'.$langs->trans("HideDetails").'">';
|
|
print '<span class="fas fa-minus"></span> '.$langs->trans("HideDetails");
|
|
print '</button>';
|
|
print '</div>';
|
|
|
|
print '<div class="div-table-responsive-no-min">';
|
|
print '<table class="noborder centpercent" id="tracking-table">';
|
|
print '<tr class="liste_titre">';
|
|
print '<th>'.$langs->trans("Product").'</th>';
|
|
print '<th class="right">'.$langs->trans("QtyOrdered").'</th>';
|
|
print '<th class="right">'.$langs->trans("QtyDelivered").'</th>';
|
|
print '<th class="right">'.$langs->trans("QtyRemaining").'</th>';
|
|
print '<th>'.$langs->trans("Status").'</th>';
|
|
print '</tr>';
|
|
|
|
// 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
|
|
$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_additional,";
|
|
// Entfällt
|
|
$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_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 '<tr class="oddeven">';
|
|
|
|
// Produkt mit Toggle
|
|
print '<td>';
|
|
if ($hasDetails) {
|
|
print '<span class="tracking-toggle" onclick="toggleTrackingDetails(\'tdetail_'.$detailRowId.'\')" style="cursor:pointer; margin-right:5px;">';
|
|
print '<span class="fas fa-chevron-right tracking-arrow" id="tarrow_tdetail_'.$detailRowId.'"></span>';
|
|
print '</span>';
|
|
}
|
|
if ($obj->fk_product > 0) {
|
|
print '<a href="'.DOL_URL_ROOT.'/product/card.php?id='.$obj->fk_product.'">';
|
|
print img_picto('', 'product', 'class="pictofixedwidth"');
|
|
print $obj->product_ref.' - '.$obj->product_label;
|
|
print '</a>';
|
|
} 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 '<span class="opacitymedium">'.$desc.'</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Bestellt (mit Badges für Mehraufwand/Entfällt)
|
|
print '<td class="right">';
|
|
print '<strong>'.formatQty($effective_ordered).'</strong>';
|
|
if ($qty_additional > 0) {
|
|
print ' <span class="badge" style="background-color: #28a745; color: #fff; font-size: 0.75em;" title="'.$langs->trans("Mehraufwand").'">+'.formatQty($qty_additional).'</span>';
|
|
}
|
|
if ($qty_omitted > 0) {
|
|
print ' <span class="badge" style="background-color: #dc3545; color: #fff; font-size: 0.75em;" title="'.$langs->trans("Entfaellt").'">-'.formatQty($qty_omitted).'</span>';
|
|
}
|
|
print '</td>';
|
|
$total_ordered += $effective_ordered;
|
|
|
|
// Geliefert/Erfasst
|
|
print '<td class="right">'.formatQty($obj->qty_delivered).'</td>';
|
|
$total_delivered += $obj->qty_delivered;
|
|
|
|
// Verbleibend
|
|
print '<td class="right">';
|
|
if ($qty_remaining > 0) {
|
|
print '<span class="badge badge-warning">'.formatQty($qty_remaining).'</span>';
|
|
} elseif ($qty_remaining == 0) {
|
|
print '<span class="badge badge-success">0</span>';
|
|
} else {
|
|
print '<span class="badge badge-info">'.formatQty($qty_remaining).'</span>';
|
|
}
|
|
print '</td>';
|
|
$total_remaining += $qty_remaining;
|
|
|
|
// Status
|
|
print '<td>';
|
|
if ($qty_remaining <= 0) {
|
|
print '<span class="badge badge-success">'.$langs->trans("TrackingDone").'</span>';
|
|
} elseif ($obj->qty_delivered > 0) {
|
|
print '<span class="badge badge-warning">'.$langs->trans("TrackingPartial").'</span>';
|
|
} else {
|
|
print '<span class="badge badge-secondary">'.$langs->trans("TrackingOpen").'</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
print '</tr>';
|
|
|
|
// Detail-Zeile (standardmäßig eingeklappt)
|
|
if ($hasDetails) {
|
|
print '<tr id="tdetail_'.$detailRowId.'" class="tracking-detail-row" style="display:none;">';
|
|
print '<td colspan="5" style="padding: 0 0 0 30px; background-color: #fafafa;">';
|
|
|
|
// Sub-Tabelle für Details
|
|
print '<table class="noborder" style="width:100%; margin: 5px 0;">';
|
|
print '<tr class="liste_titre" style="background-color: #f0f0f0;">';
|
|
print '<th style="width:120px;">'.$langs->trans("Stundenzettel").'</th>';
|
|
print '<th style="width:100px;">'.$langs->trans("Date").'</th>';
|
|
print '<th style="width:100px;">'.$langs->trans("Type").'</th>';
|
|
print '<th class="right" style="width:80px;">'.$langs->trans("Qty").'</th>';
|
|
print '<th>'.$langs->trans("Reason").'</th>';
|
|
print '</tr>';
|
|
|
|
foreach ($details as $det) {
|
|
print '<tr class="oddeven">';
|
|
|
|
// Stundenzettel-Referenz mit Link
|
|
print '<td>';
|
|
print '<a href="'.dol_buildpath('/stundenzettel/card.php', 1).'?id='.$det->fk_stundenzettel.'">';
|
|
print $det->stz_ref;
|
|
print '</a>';
|
|
print '</td>';
|
|
|
|
// Datum
|
|
print '<td>'.dol_print_date($db->jdate($det->date_stundenzettel), 'day').'</td>';
|
|
|
|
// Typ (origin)
|
|
print '<td>';
|
|
switch ($det->origin) {
|
|
case 'order':
|
|
case 'added':
|
|
print '<span class="badge badge-primary">'.$langs->trans("QtyDelivered").'</span>';
|
|
break;
|
|
case 'additional':
|
|
print '<span class="badge badge-success">'.$langs->trans("Mehraufwand").'</span>';
|
|
break;
|
|
case 'omitted':
|
|
print '<span class="badge badge-danger">'.$langs->trans("Entfaellt").'</span>';
|
|
break;
|
|
default:
|
|
print $det->origin;
|
|
}
|
|
print '</td>';
|
|
|
|
// Menge
|
|
print '<td class="right">'.formatQty($det->qty_done).'</td>';
|
|
|
|
// Grund/Beschreibung
|
|
print '<td>';
|
|
if (!empty($det->description)) {
|
|
print dol_escape_htmltag($det->description);
|
|
} else {
|
|
print '<span class="opacitymedium">-</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
print '</tr>';
|
|
}
|
|
|
|
print '</table>';
|
|
print '</td>';
|
|
print '</tr>';
|
|
}
|
|
}
|
|
|
|
// Summenzeile
|
|
print '<tr class="liste_total">';
|
|
print '<td><strong>'.$langs->trans("Total").'</strong></td>';
|
|
print '<td class="right"><strong>'.formatQty($total_ordered).'</strong></td>';
|
|
print '<td class="right"><strong>'.formatQty($total_delivered).'</strong></td>';
|
|
print '<td class="right"><strong>'.formatQty($total_remaining).'</strong></td>';
|
|
print '<td></td>';
|
|
print '</tr>';
|
|
|
|
} else {
|
|
print '<tr class="oddeven"><td colspan="5" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
|
|
}
|
|
}
|
|
|
|
print '</table>';
|
|
print '</div>';
|
|
|
|
// JavaScript für Tracking-Details
|
|
print '<script>
|
|
function toggleTrackingDetails(detailId) {
|
|
var detailRow = document.getElementById(detailId);
|
|
var arrow = document.getElementById("tarrow_" + detailId);
|
|
if (detailRow) {
|
|
var isHidden = detailRow.style.display === "none";
|
|
detailRow.style.display = isHidden ? "" : "none";
|
|
if (arrow) {
|
|
arrow.classList.toggle("fa-chevron-right", !isHidden);
|
|
arrow.classList.toggle("fa-chevron-down", isHidden);
|
|
}
|
|
}
|
|
}
|
|
|
|
function expandAllTrackingDetails() {
|
|
var detailRows = document.querySelectorAll(".tracking-detail-row");
|
|
detailRows.forEach(function(row) {
|
|
row.style.display = "";
|
|
var arrow = document.getElementById("tarrow_" + row.id);
|
|
if (arrow) {
|
|
arrow.classList.remove("fa-chevron-right");
|
|
arrow.classList.add("fa-chevron-down");
|
|
}
|
|
});
|
|
}
|
|
|
|
function collapseAllTrackingDetails() {
|
|
var detailRows = document.querySelectorAll(".tracking-detail-row");
|
|
detailRows.forEach(function(row) {
|
|
row.style.display = "none";
|
|
var arrow = document.getElementById("tarrow_" + row.id);
|
|
if (arrow) {
|
|
arrow.classList.remove("fa-chevron-down");
|
|
arrow.classList.add("fa-chevron-right");
|
|
}
|
|
});
|
|
}
|
|
</script>';
|
|
|
|
// Button: In Rechnung übertragen (nur wenn alles erledigt)
|
|
if ($total_remaining <= 0 && $total_delivered > 0) {
|
|
print '<div class="center" style="margin-top:20px;">';
|
|
print '<a class="butAction" href="?id='.$order->id.'&action=transfer_to_invoice">';
|
|
print img_picto('', 'bill', 'class="pictofixedwidth"').$langs->trans("TransferToInvoice");
|
|
print '</a>';
|
|
print '</div>';
|
|
}
|
|
}
|
|
|
|
print '</div>'; // fichecenter
|
|
|
|
dol_fiche_end();
|
|
|
|
llxFooter();
|
|
$db->close();
|