PWA (neue Dateien): - Vollständige Progressive Web App mit Token-basierter Auth - 4 Swipe-Panels: Alle STZ, Stundenzettel, Produktliste, Lieferauflistung - Kundensuche, Leistungen-Accordion, Mehraufwand-Sektion - Produkt-Übernahme aus Auftrag + Mehraufwand in STZ - Service Worker, Manifest, App-Icons für Installation Desktop-Änderungen: - Produktliste: Checkboxen immer sichtbar (außer bereits auf STZ) - Lieferauflistung: Vereinfachte Ansicht (nur Verbaut-Spalte) - Admin: PWA-Link in Einstellungen - Sprachdatei: PWA-Übersetzungen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2457 lines
126 KiB
PHP
Executable file
2457 lines
126 KiB
PHP
Executable file
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* Stundenzettel - Hauptformular (Card)
|
|
*/
|
|
|
|
// 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.'/product/class/product.class.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/core/class/doleditor.class.php';
|
|
dol_include_once('/stundenzettel/class/stundenzettel.class.php');
|
|
dol_include_once('/stundenzettel/lib/stundenzettel.lib.php');
|
|
|
|
// Load translation files
|
|
$langs->loadLangs(array("stundenzettel@stundenzettel", "orders", "products"));
|
|
|
|
/**
|
|
* Generiert Zeitoptionen im 15-Minuten-Takt
|
|
* @param string $selected Ausgewählte Zeit (HH:MM)
|
|
* @param string $defaultHour Standard-Stunde
|
|
* @return string HTML-Options
|
|
*/
|
|
function getTimeOptions($selected = '', $defaultHour = '08') {
|
|
$options = '<option value="">--:--</option>';
|
|
for ($h = 6; $h <= 22; $h++) {
|
|
for ($m = 0; $m < 60; $m += 15) {
|
|
$time = sprintf('%02d:%02d', $h, $m);
|
|
$isSelected = ($selected == $time) || (empty($selected) && $h == intval($defaultHour) && $m == 0);
|
|
$options .= '<option value="'.$time.'"'.($isSelected ? ' selected' : '').'>'.$time.'</option>';
|
|
}
|
|
}
|
|
return $options;
|
|
}
|
|
|
|
/**
|
|
* 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, ',', '.');
|
|
}
|
|
$formatted = rtrim(rtrim(number_format($qty, 2, ',', '.'), '0'), ',');
|
|
return $formatted;
|
|
}
|
|
|
|
// Get parameters
|
|
$id = GETPOST('id', 'int');
|
|
$ref = GETPOST('ref', 'alpha');
|
|
$action = GETPOST('action', 'aZ09');
|
|
$confirm = GETPOST('confirm', 'alpha');
|
|
$fk_commande = GETPOST('fk_commande', 'int');
|
|
$subtab = GETPOST('tab', 'aZ09') ?: 'leistungen'; // Interner Sub-Tab auf dieser Seite
|
|
|
|
// Security check
|
|
if (!$user->hasRight('stundenzettel', 'read')) {
|
|
accessforbidden();
|
|
}
|
|
|
|
$object = new Stundenzettel($db);
|
|
|
|
// Load object
|
|
if ($id > 0 || !empty($ref)) {
|
|
$result = $object->fetch($id, $ref);
|
|
if ($result <= 0) {
|
|
dol_print_error($db, $object->error);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// Permissions - Basis-Berechtigungen
|
|
$permissiontoread = $user->hasRight('stundenzettel', 'read');
|
|
$permissiontoreadall = $user->hasRight('stundenzettel', 'read', 'all') || $user->admin;
|
|
$permissiontowrite = $user->hasRight('stundenzettel', 'write');
|
|
$permissiontowriteall = $user->hasRight('stundenzettel', 'write', 'all') || $user->admin;
|
|
$permissiontodelete = $user->hasRight('stundenzettel', 'delete');
|
|
$permissiontodeleteall = $user->hasRight('stundenzettel', 'delete', 'all') || $user->admin;
|
|
$permissiontovalidate = $user->hasRight('stundenzettel', 'validate');
|
|
|
|
// Prüfen ob der aktuelle Stundenzettel dem Benutzer gehört
|
|
$isOwner = ($object->id > 0 && $object->fk_user_author == $user->id);
|
|
|
|
// Effektive Berechtigungen für diesen Stundenzettel
|
|
$permissiontoadd = $permissiontowrite && ($isOwner || $permissiontowriteall || $action == 'create');
|
|
$permissiontodeleteobj = $permissiontodelete && ($isOwner || $permissiontodeleteall);
|
|
|
|
// Zugriffskontrolle: Wenn geladen und nicht berechtigt -> Zugriff verweigern
|
|
if ($object->id > 0 && !$isOwner && !$permissiontoreadall) {
|
|
accessforbidden('Sie haben keine Berechtigung, diesen Stundenzettel anzuzeigen.');
|
|
}
|
|
|
|
/*
|
|
* Actions
|
|
*/
|
|
|
|
// Create
|
|
if ($action == 'add' && $permissiontoadd) {
|
|
$object->fk_commande = GETPOST('fk_commande', 'int');
|
|
|
|
// Datum aus Formular holen - selectDate generiert: {prefix}day, {prefix}month, {prefix}year
|
|
$dateYear = GETPOST('date_stundenzetteljahr', 'int'); // DE locale
|
|
if (empty($dateYear)) $dateYear = GETPOST('date_stundenzettelselectyear', 'int'); // select version
|
|
if (empty($dateYear)) $dateYear = GETPOST('date_stundenzetteyear', 'int'); // typo fallback
|
|
if (empty($dateYear)) $dateYear = GETPOST('date_stundenzettelyear', 'int'); // correct year suffix
|
|
|
|
$dateMonth = GETPOST('date_stundenzettelmonth', 'int');
|
|
if (empty($dateMonth)) $dateMonth = GETPOST('date_stundenzettelselectmonth', 'int');
|
|
|
|
$dateDay = GETPOST('date_stundenzettelday', 'int');
|
|
if (empty($dateDay)) $dateDay = GETPOST('date_stundenzettelselectday', 'int');
|
|
|
|
// Debug: Log welche Werte ankommen
|
|
dol_syslog("Stundenzettel create: day=$dateDay, month=$dateMonth, year=$dateYear", LOG_DEBUG);
|
|
|
|
if ($dateYear > 0 && $dateMonth > 0 && $dateDay > 0) {
|
|
$object->date_stundenzettel = dol_mktime(0, 0, 0, $dateMonth, $dateDay, $dateYear);
|
|
} else {
|
|
// Fallback: Wenn kein gültiges Datum, nehme heute
|
|
$object->date_stundenzettel = dol_now();
|
|
}
|
|
|
|
$object->note_private = GETPOST('note_private', 'restricthtml');
|
|
$object->note_public = GETPOST('note_public', 'restricthtml');
|
|
|
|
// Get socid from order and check if released
|
|
$order = new Commande($db);
|
|
$orderReleased = false;
|
|
if ($order->fetch($object->fk_commande) > 0) {
|
|
$object->fk_soc = $order->socid;
|
|
// Prüfe Stundenzettel-Status des Auftrags
|
|
$order->fetch_optionals();
|
|
$stzStatus = isset($order->array_options['options_stundenzettel_status']) ? (int)$order->array_options['options_stundenzettel_status'] : 0;
|
|
if ($stzStatus >= 1) {
|
|
$orderReleased = true;
|
|
}
|
|
}
|
|
|
|
if ($orderReleased) {
|
|
setEventMessages($langs->trans('ErrorStundenzettelReleased'), null, 'errors');
|
|
header('Location: '.dol_buildpath('/stundenzettel/stundenzettel_commande.php?id='.$object->fk_commande.'&tab=stundenzettel&noredirect=1', 1));
|
|
exit;
|
|
} elseif (empty($object->fk_commande)) {
|
|
setEventMessages($langs->trans('ErrorNoOrder'), null, 'errors');
|
|
$action = 'create';
|
|
} else {
|
|
// Prüfen ob für diesen Auftrag und dieses Datum bereits ein Stundenzettel existiert
|
|
$dateStr = dol_print_date($object->date_stundenzettel, '%Y-%m-%d');
|
|
$sqlCheck = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel";
|
|
$sqlCheck .= " WHERE fk_commande = ".((int)$object->fk_commande);
|
|
$sqlCheck .= " AND DATE(date_stundenzettel) = '".$db->escape($dateStr)."'";
|
|
$sqlCheck .= " AND status = 0"; // Nur Entwürfe
|
|
$resqlCheck = $db->query($sqlCheck);
|
|
|
|
if ($resqlCheck && $db->num_rows($resqlCheck) > 0) {
|
|
// Es existiert bereits ein Stundenzettel für diesen Tag - dorthin weiterleiten
|
|
$objExisting = $db->fetch_object($resqlCheck);
|
|
setEventMessages($langs->trans('StundenzettelExistsForDate'), null, 'warnings');
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$objExisting->rowid);
|
|
exit;
|
|
}
|
|
|
|
$result = $object->create($user);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('StundenzettelCreated'), null, 'mesgs');
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$result);
|
|
exit;
|
|
} else {
|
|
setEventMessages($object->error, $object->errors, 'errors');
|
|
$action = 'create';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update
|
|
if ($action == 'update' && $permissiontoadd) {
|
|
$object->date_stundenzettel = dol_mktime(0, 0, 0, GETPOST('date_stundenzettelmonth', 'int'), GETPOST('date_stundenzettelday', 'int'), GETPOST('date_stundenzettleyear', 'int'));
|
|
$object->note_private = GETPOST('note_private', 'restricthtml');
|
|
$object->note_public = GETPOST('note_public', 'restricthtml');
|
|
|
|
$result = $object->update($user);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordModified'), null, 'mesgs');
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=notes');
|
|
exit;
|
|
} else {
|
|
setEventMessages($object->error, $object->errors, 'errors');
|
|
}
|
|
}
|
|
|
|
// Validate
|
|
if ($action == 'confirm_validate' && $confirm == 'yes' && $permissiontovalidate) {
|
|
$result = $object->validate($user);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('StundenzettelValidated'), null, 'mesgs');
|
|
// Netto-Wert aller Stundenzettel des Auftrags neu berechnen
|
|
updateOrderNettoSTZ($db, $object->fk_commande);
|
|
} else {
|
|
setEventMessages($object->error, $object->errors, 'errors');
|
|
}
|
|
}
|
|
|
|
// Set to draft
|
|
if ($action == 'confirm_setdraft' && $confirm == 'yes' && $permissiontoadd) {
|
|
$result = $object->setDraft($user);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordModified'), null, 'mesgs');
|
|
// Netto-Wert neu berechnen (da dieser Stundenzettel nicht mehr freigegeben ist)
|
|
updateOrderNettoSTZ($db, $object->fk_commande);
|
|
} else {
|
|
setEventMessages($object->error, $object->errors, 'errors');
|
|
}
|
|
}
|
|
|
|
// Delete
|
|
if ($action == 'confirm_delete' && $confirm == 'yes' && $permissiontodeleteobj) {
|
|
$fk_commande = $object->fk_commande; // Speichern vor dem Löschen
|
|
$result = $object->delete($user);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('StundenzettelDeleted'), null, 'mesgs');
|
|
// Netto-Wert neu berechnen
|
|
if ($fk_commande > 0) {
|
|
updateOrderNettoSTZ($db, $fk_commande);
|
|
}
|
|
// Weiterleitung zur Stundenzettel-Liste des Auftrags
|
|
if ($fk_commande > 0) {
|
|
header('Location: '.dol_buildpath('/stundenzettel/stundenzettel_commande.php?id='.$fk_commande.'&tab=stundenzettel&noredirect=1', 1));
|
|
} else {
|
|
header('Location: '.dol_buildpath('/stundenzettel/list.php', 1));
|
|
}
|
|
exit;
|
|
} else {
|
|
setEventMessages($object->error, $object->errors, 'errors');
|
|
}
|
|
}
|
|
|
|
// Add Leistung
|
|
if ($action == 'add_leistung' && $permissiontoadd) {
|
|
// Datum kann als einzelnes Feld (leistung_date) oder als separate Felder kommen
|
|
$leistung_date_str = GETPOST('leistung_date', 'alpha');
|
|
if (!empty($leistung_date_str)) {
|
|
// Format: Y-m-d
|
|
$date = strtotime($leistung_date_str);
|
|
} else {
|
|
$date = dol_mktime(0, 0, 0, GETPOST('leistung_datemonth', 'int'), GETPOST('leistung_dateday', 'int'), GETPOST('leistung_dateyear', 'int'));
|
|
}
|
|
|
|
$time_start = GETPOST('time_start', 'alpha');
|
|
$time_end = GETPOST('time_end', 'alpha');
|
|
$description = GETPOST('leistung_description', 'restricthtml');
|
|
$fk_product = GETPOST('fk_product', 'int');
|
|
|
|
$result = $object->addLeistung($user, $date, $time_start, $time_end, $description, $fk_product);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=leistungen');
|
|
exit;
|
|
}
|
|
|
|
// Update Leistung
|
|
if ($action == 'update_leistung' && $permissiontoadd) {
|
|
$leistung_id = GETPOST('leistung_id', 'int');
|
|
|
|
// Datum kann als einzelnes Feld (leistung_date) oder als separate Felder kommen
|
|
$leistung_date_str = GETPOST('leistung_date', 'alpha');
|
|
if (!empty($leistung_date_str)) {
|
|
$date = strtotime($leistung_date_str);
|
|
} else {
|
|
$date = dol_mktime(0, 0, 0, GETPOST('leistung_datemonth', 'int'), GETPOST('leistung_dateday', 'int'), GETPOST('leistung_dateyear', 'int'));
|
|
}
|
|
|
|
$time_start = GETPOST('time_start', 'alpha');
|
|
$time_end = GETPOST('time_end', 'alpha');
|
|
$description = GETPOST('leistung_description', 'restricthtml');
|
|
$fk_product = GETPOST('fk_product', 'int');
|
|
|
|
$result = $object->updateLeistung($leistung_id, $date, $time_start, $time_end, $description, $fk_product);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordModified'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=leistungen');
|
|
exit;
|
|
}
|
|
|
|
// Delete Leistung
|
|
if ($action == 'confirm_delete_leistung' && $confirm == 'yes' && $permissiontoadd) {
|
|
$leistung_id = GETPOST('leistung_id', 'int');
|
|
$result = $object->deleteLeistung($leistung_id);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=leistungen');
|
|
exit;
|
|
}
|
|
|
|
// Update product qty
|
|
if ($action == 'update_qty' && $permissiontoadd) {
|
|
$line_id = GETPOST('line_id', 'int');
|
|
$qty_done = (float)price2num(GETPOST('qty_done', 'alpha'));
|
|
|
|
$result = $object->updateProductQty($line_id, $qty_done);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordModified'), null, 'mesgs');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products');
|
|
exit;
|
|
}
|
|
|
|
// Produkt hinzufügen (aus Katalog oder Freitext)
|
|
// Logik: Wenn Produkt im Auftrag existiert → als Verbrauch zur Auftragszeile
|
|
// Wenn Produkt NICHT im Auftrag → als Mehraufwand hinzufügen
|
|
if ($action == 'add_product' && $permissiontoadd) {
|
|
$fk_product = GETPOST('add_product_id', 'int');
|
|
$qty = (float)price2num(GETPOST('add_product_qty', 'alpha'));
|
|
$description = GETPOST('add_product_description', 'restricthtml');
|
|
|
|
if ($fk_product > 0 || !empty($description)) {
|
|
$productHandled = false;
|
|
|
|
// Bei Katalog-Produkten: Prüfen ob das Produkt im Auftrag existiert
|
|
if ($fk_product > 0) {
|
|
$order = new Commande($db);
|
|
if ($order->fetch($object->fk_commande) > 0) {
|
|
// Prüfen ob dieses Produkt in einer Auftragszeile existiert
|
|
$sqlOrderLine = "SELECT cd.rowid as commandedet_id FROM ".MAIN_DB_PREFIX."commandedet cd";
|
|
$sqlOrderLine .= " WHERE cd.fk_commande = ".((int)$order->id);
|
|
$sqlOrderLine .= " AND cd.fk_product = ".((int)$fk_product);
|
|
$sqlOrderLine .= " LIMIT 1";
|
|
$resqlOrderLine = $db->query($sqlOrderLine);
|
|
|
|
if ($resqlOrderLine && ($objOrderLine = $db->fetch_object($resqlOrderLine))) {
|
|
// Produkt existiert im Auftrag - als Verbrauch hinzufügen
|
|
$commandedet_id = $objOrderLine->commandedet_id;
|
|
|
|
// Prüfen ob schon eine Zeile für diesen Stundenzettel existiert
|
|
$sqlExisting = "SELECT rowid, qty_done FROM ".MAIN_DB_PREFIX."stundenzettel_product";
|
|
$sqlExisting .= " WHERE fk_stundenzettel = ".((int)$object->id);
|
|
$sqlExisting .= " AND fk_commandedet = ".((int)$commandedet_id);
|
|
$sqlExisting .= " AND origin IN ('order', 'added')";
|
|
$sqlExisting .= " LIMIT 1";
|
|
$resqlExisting = $db->query($sqlExisting);
|
|
|
|
if ($resqlExisting && ($objExisting = $db->fetch_object($resqlExisting))) {
|
|
// Zeile existiert - Menge erhöhen
|
|
$newQty = (float)$objExisting->qty_done + $qty;
|
|
$result = $object->updateProductQty($objExisting->rowid, $newQty);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordModified'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
} else {
|
|
// Neue Zeile als Verbrauch hinzufügen
|
|
$product = new Product($db);
|
|
$product->fetch($fk_product);
|
|
|
|
$result = $object->addProduct(
|
|
$fk_product,
|
|
$commandedet_id, // fk_commandedet
|
|
0, // fk_manager_line
|
|
0, // qty_original
|
|
$qty, // qty_done
|
|
'added', // origin = Verbrauch
|
|
$description
|
|
);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
}
|
|
$productHandled = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wenn Produkt nicht im Auftrag - als Verbaut hinzufügen (erscheint in Produktliste)
|
|
if (!$productHandled) {
|
|
// Prüfen ob bereits auf diesem Stundenzettel existiert
|
|
$existingLineId = 0;
|
|
$existingQty = 0;
|
|
if ($fk_product > 0) {
|
|
$sqlCheck = "SELECT rowid, qty_done FROM ".MAIN_DB_PREFIX."stundenzettel_product";
|
|
$sqlCheck .= " WHERE fk_stundenzettel = ".((int)$object->id);
|
|
$sqlCheck .= " AND fk_product = ".((int)$fk_product);
|
|
$sqlCheck .= " AND origin = 'added'";
|
|
$sqlCheck .= " LIMIT 1";
|
|
} else {
|
|
$sqlCheck = "SELECT rowid, qty_done FROM ".MAIN_DB_PREFIX."stundenzettel_product";
|
|
$sqlCheck .= " WHERE fk_stundenzettel = ".((int)$object->id);
|
|
$sqlCheck .= " AND (fk_product IS NULL OR fk_product = 0)";
|
|
$sqlCheck .= " AND origin = 'added'";
|
|
$sqlCheck .= " AND description = '".$db->escape($description)."'";
|
|
$sqlCheck .= " LIMIT 1";
|
|
}
|
|
$resqlCheck = $db->query($sqlCheck);
|
|
if ($resqlCheck && ($objCheck = $db->fetch_object($resqlCheck))) {
|
|
$existingLineId = $objCheck->rowid;
|
|
$existingQty = (float)$objCheck->qty_done;
|
|
}
|
|
|
|
if ($existingLineId > 0) {
|
|
// Existiert bereits - Menge erhöhen
|
|
$newQty = $existingQty + $qty;
|
|
$result = $object->updateProductQty($existingLineId, $newQty);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordModified'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
} else {
|
|
// Neu als Verbaut anlegen
|
|
$result = $object->addProduct(
|
|
$fk_product,
|
|
0, // fk_commandedet = NULL
|
|
0, // fk_manager_line
|
|
0, // qty_original = 0
|
|
$qty, // qty_done = Verbaut
|
|
'added', // origin = manuell hinzugefügt (Produktliste)
|
|
$description
|
|
);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
setEventMessages($langs->trans('ErrorNoProductSelected'), null, 'errors');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products');
|
|
exit;
|
|
}
|
|
|
|
// Entfällt hinzufügen (Produkt aus Auftrag oder Mehraufwand das nicht verbaut wird)
|
|
if ($action == 'add_entfaellt' && $permissiontoadd) {
|
|
$entfaellt_product_raw = GETPOST('entfaellt_product', 'alpha');
|
|
$qty = (float)price2num(GETPOST('entfaellt_qty', 'alpha'));
|
|
$reason = GETPOST('entfaellt_description', 'restricthtml');
|
|
|
|
// Prüfen ob es ein Freitext-Produkt, Mehraufwand oder normales Produkt ist
|
|
$fk_product = 0;
|
|
$freetext_description = '';
|
|
$commandedet_id = 0;
|
|
$mehraufwand_id = 0;
|
|
|
|
if (strpos($entfaellt_product_raw, 'mehraufwand_') === 0) {
|
|
// Mehraufwand-Produkt (Format: "mehraufwand_ROWID")
|
|
$mehraufwand_id = (int)substr($entfaellt_product_raw, 12);
|
|
// Produkt-ID und Beschreibung aus stundenzettel_product laden
|
|
$sqlMehr = "SELECT fk_product, description FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE rowid = ".((int)$mehraufwand_id);
|
|
$resqlMehr = $db->query($sqlMehr);
|
|
if ($resqlMehr && ($objMehr = $db->fetch_object($resqlMehr))) {
|
|
$fk_product = (int)$objMehr->fk_product;
|
|
if ($fk_product == 0) {
|
|
$freetext_description = $objMehr->description;
|
|
}
|
|
}
|
|
} elseif (strpos($entfaellt_product_raw, 'freetext_') === 0) {
|
|
// Freitext-Produkt aus dem Auftrag
|
|
$commandedet_id = (int)substr($entfaellt_product_raw, 9);
|
|
// Beschreibung aus commandedet laden
|
|
$sqlDesc = "SELECT description FROM ".MAIN_DB_PREFIX."commandedet WHERE rowid = ".((int)$commandedet_id);
|
|
$resqlDesc = $db->query($sqlDesc);
|
|
if ($resqlDesc && ($objDesc = $db->fetch_object($resqlDesc))) {
|
|
$freetext_description = $objDesc->description;
|
|
}
|
|
} else {
|
|
$fk_product = (int)$entfaellt_product_raw;
|
|
}
|
|
|
|
// Beschreibung: Freitext-Beschreibung + Grund
|
|
$description = $reason;
|
|
if (!empty($freetext_description)) {
|
|
$description = strip_tags($freetext_description) . (!empty($reason) ? ' - ' . $reason : '');
|
|
}
|
|
|
|
$error = 0;
|
|
|
|
if ($fk_product > 0 || !empty($freetext_description)) {
|
|
// Server-seitige Validierung: Prüfen ob Menge noch verfügbar ist
|
|
if ($mehraufwand_id > 0) {
|
|
// Mehraufwand-Validierung: Prüfe verfügbare Menge
|
|
$sqlCheck = "SELECT qty, qty_done FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE rowid = ".((int)$mehraufwand_id);
|
|
$resqlCheck = $db->query($sqlCheck);
|
|
if ($resqlCheck && ($objCheck = $db->fetch_object($resqlCheck))) {
|
|
$qty_available = $objCheck->qty - $objCheck->qty_done;
|
|
if ($qty > $qty_available) {
|
|
setEventMessages($langs->trans('ErrorQtyExceedsAvailable', $qty_available), null, 'errors');
|
|
$error++;
|
|
}
|
|
}
|
|
} elseif ($object->fk_commande > 0) {
|
|
if ($fk_product > 0) {
|
|
// Produkt-Validierung
|
|
$sqlCheck = "SELECT cd.qty,";
|
|
$sqlCheck .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
|
$sqlCheck .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
|
$sqlCheck .= " WHERE sp.fk_commandedet = cd.rowid AND sp.origin IN ('order', 'added')), 0) as qty_used,";
|
|
$sqlCheck .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2";
|
|
$sqlCheck .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel";
|
|
$sqlCheck .= " WHERE (sp2.fk_commandedet = cd.rowid OR (sp2.fk_product = cd.fk_product AND cd.fk_product > 0)) AND sp2.origin = 'omitted' AND s2.fk_commande = ".((int)$object->fk_commande)."), 0) as qty_omitted";
|
|
$sqlCheck .= " FROM ".MAIN_DB_PREFIX."commandedet as cd";
|
|
$sqlCheck .= " WHERE cd.fk_commande = ".((int)$object->fk_commande);
|
|
$sqlCheck .= " AND cd.fk_product = ".((int)$fk_product);
|
|
$sqlCheck .= " LIMIT 1";
|
|
|
|
$resqlCheck = $db->query($sqlCheck);
|
|
if ($resqlCheck && ($objCheck = $db->fetch_object($resqlCheck))) {
|
|
$qty_available = $objCheck->qty - $objCheck->qty_used - $objCheck->qty_omitted;
|
|
if ($qty > $qty_available) {
|
|
setEventMessages($langs->trans('ErrorQtyExceedsAvailable', $qty_available), null, 'errors');
|
|
$error++;
|
|
}
|
|
}
|
|
} elseif (!empty($commandedet_id)) {
|
|
// Freitext-Produkt Validierung
|
|
$sqlCheck = "SELECT cd.qty,";
|
|
$sqlCheck .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
|
$sqlCheck .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
|
$sqlCheck .= " WHERE sp.fk_commandedet = cd.rowid AND sp.origin IN ('order', 'added')), 0) as qty_used,";
|
|
$sqlCheck .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2";
|
|
$sqlCheck .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel";
|
|
$sqlCheck .= " WHERE sp2.fk_commandedet = cd.rowid AND sp2.origin = 'omitted' AND s2.fk_commande = ".((int)$object->fk_commande)."), 0) as qty_omitted";
|
|
$sqlCheck .= " FROM ".MAIN_DB_PREFIX."commandedet as cd";
|
|
$sqlCheck .= " WHERE cd.rowid = ".((int)$commandedet_id);
|
|
$sqlCheck .= " LIMIT 1";
|
|
|
|
$resqlCheck = $db->query($sqlCheck);
|
|
if ($resqlCheck && ($objCheck = $db->fetch_object($resqlCheck))) {
|
|
$qty_available = $objCheck->qty - $objCheck->qty_used - $objCheck->qty_omitted;
|
|
if ($qty > $qty_available) {
|
|
setEventMessages($langs->trans('ErrorQtyExceedsAvailable', $qty_available), null, 'errors');
|
|
$error++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$error) {
|
|
if ($mehraufwand_id > 0) {
|
|
// Mehraufwand: Menge vom qty_done erhöhen statt neuen Eintrag erstellen
|
|
// Oder: Menge vom Mehraufwand reduzieren und als Entfällt anlegen
|
|
// Wir reduzieren qty des Mehraufwands und legen einen neuen Entfällt-Eintrag an
|
|
$sqlUpdateMehr = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_product SET qty = qty - ".((int)$qty);
|
|
$sqlUpdateMehr .= " WHERE rowid = ".((int)$mehraufwand_id);
|
|
$db->query($sqlUpdateMehr);
|
|
|
|
// Entfällt-Eintrag mit Hinweis auf Mehraufwand anlegen
|
|
$entfaelltDesc = $langs->trans("Mehraufwand").': '.$description;
|
|
if (!empty($reason)) {
|
|
$entfaelltDesc .= ' - '.$reason;
|
|
}
|
|
$result = $object->addProduct(
|
|
$fk_product,
|
|
0, // fk_commandedet
|
|
0, // fk_manager_line
|
|
0, // qty_original
|
|
$qty, // qty_done (Menge die entfällt)
|
|
'omitted', // origin (entfällt)
|
|
$entfaelltDesc // description (Grund)
|
|
);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
} else {
|
|
// Produkt zum Stundenzettel hinzufügen mit origin='omitted'
|
|
$result = $object->addProduct(
|
|
$fk_product,
|
|
$commandedet_id, // fk_commandedet (Verknüpfung zur Auftragszeile)
|
|
0, // fk_manager_line
|
|
0, // qty_original
|
|
$qty, // qty_done (Menge die entfällt)
|
|
'omitted', // origin (entfällt)
|
|
$description // description (Grund)
|
|
);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
setEventMessages($langs->trans('ErrorNoProductSelected'), null, 'errors');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products');
|
|
exit;
|
|
}
|
|
|
|
// Entfällt-Produkt löschen
|
|
if ($action == 'confirm_delete_entfaellt' && $confirm == 'yes' && $permissiontoadd) {
|
|
$line_id = GETPOST('line_id', 'int');
|
|
$result = $object->deleteProduct($line_id);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products');
|
|
exit;
|
|
}
|
|
|
|
// Rücknahme hinzufügen (bereits verbautes Produkt wird zurückgenommen)
|
|
if ($action == 'add_ruecknahme' && $permissiontoadd) {
|
|
$ruecknahme_product_raw = GETPOST('ruecknahme_product', 'alpha');
|
|
$qty = (float)price2num(GETPOST('ruecknahme_qty', 'alpha'));
|
|
$reason = GETPOST('ruecknahme_description', 'restricthtml');
|
|
|
|
// Prüfen ob es ein Freitext-Produkt oder normales Produkt ist
|
|
$fk_product = 0;
|
|
$freetext_description = '';
|
|
$freetext_label = ''; // Label für Freitext-Produkte (wird in product_label gespeichert)
|
|
$commandedet_id = 0;
|
|
|
|
if (strpos($ruecknahme_product_raw, 'freetext_') === 0) {
|
|
// Freitext-Produkt aus dem Auftrag
|
|
$commandedet_id = (int)substr($ruecknahme_product_raw, 9);
|
|
// Beschreibung aus commandedet laden
|
|
$sqlDesc = "SELECT description FROM ".MAIN_DB_PREFIX."commandedet WHERE rowid = ".((int)$commandedet_id);
|
|
$resqlDesc = $db->query($sqlDesc);
|
|
if ($resqlDesc && ($objDesc = $db->fetch_object($resqlDesc))) {
|
|
$freetext_description = $objDesc->description;
|
|
$freetext_label = strip_tags($objDesc->description); // Label = Freitext ohne HTML
|
|
}
|
|
} elseif (strpos($ruecknahme_product_raw, 'mehraufwand_') === 0) {
|
|
// Freitext-Mehraufwand aus stundenzettel_product
|
|
$sp_rowid = (int)substr($ruecknahme_product_raw, 12);
|
|
$sqlDesc = "SELECT description, product_label FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE rowid = ".((int)$sp_rowid);
|
|
$resqlDesc = $db->query($sqlDesc);
|
|
if ($resqlDesc && ($objDesc = $db->fetch_object($resqlDesc))) {
|
|
$freetext_description = $objDesc->description;
|
|
// Bei Freitext-Mehraufwand ist product_label leer, also description als Label nutzen
|
|
$freetext_label = !empty($objDesc->product_label) ? $objDesc->product_label : strip_tags($objDesc->description);
|
|
}
|
|
} else {
|
|
$fk_product = (int)$ruecknahme_product_raw;
|
|
}
|
|
|
|
// Beschreibung für die Rücknahme: nur der Grund (das Label steht separat)
|
|
$description = $reason;
|
|
|
|
$error = 0;
|
|
|
|
if ($fk_product > 0 || !empty($freetext_description)) {
|
|
// Server-seitige Validierung: Prüfen ob Menge bereits verbaut wurde
|
|
if ($object->fk_commande > 0) {
|
|
if ($fk_product > 0) {
|
|
// Produkt-Validierung: Prüfe bereits verbaute Menge
|
|
$sqlCheck = "SELECT COALESCE(SUM(sp.qty_done), 0) as qty_delivered,";
|
|
$sqlCheck .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2";
|
|
$sqlCheck .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel";
|
|
$sqlCheck .= " WHERE sp2.fk_product = ".((int)$fk_product)." AND sp2.origin = 'returned' AND s2.fk_commande = ".((int)$object->fk_commande)."), 0) as qty_already_returned";
|
|
$sqlCheck .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
|
$sqlCheck .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
|
$sqlCheck .= " WHERE sp.fk_product = ".((int)$fk_product);
|
|
$sqlCheck .= " AND sp.origin IN ('order', 'added')";
|
|
$sqlCheck .= " AND s.fk_commande = ".((int)$object->fk_commande);
|
|
|
|
$resqlCheck = $db->query($sqlCheck);
|
|
if ($resqlCheck && ($objCheck = $db->fetch_object($resqlCheck))) {
|
|
$qty_available = $objCheck->qty_delivered - $objCheck->qty_already_returned;
|
|
if ($qty > $qty_available) {
|
|
setEventMessages($langs->trans('ErrorQtyExceedsAvailable', $qty_available), null, 'errors');
|
|
$error++;
|
|
}
|
|
}
|
|
} elseif (!empty($commandedet_id)) {
|
|
// Freitext-Produkt Validierung
|
|
$sqlCheck = "SELECT COALESCE(SUM(sp.qty_done), 0) as qty_delivered,";
|
|
$sqlCheck .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2";
|
|
$sqlCheck .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel";
|
|
$sqlCheck .= " WHERE sp2.fk_commandedet = ".((int)$commandedet_id)." AND sp2.origin = 'returned' AND s2.fk_commande = ".((int)$object->fk_commande)."), 0) as qty_already_returned";
|
|
$sqlCheck .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
|
$sqlCheck .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
|
$sqlCheck .= " WHERE sp.fk_commandedet = ".((int)$commandedet_id);
|
|
$sqlCheck .= " AND sp.origin IN ('order', 'added')";
|
|
$sqlCheck .= " AND s.fk_commande = ".((int)$object->fk_commande);
|
|
|
|
$resqlCheck = $db->query($sqlCheck);
|
|
if ($resqlCheck && ($objCheck = $db->fetch_object($resqlCheck))) {
|
|
$qty_available = $objCheck->qty_delivered - $objCheck->qty_already_returned;
|
|
if ($qty > $qty_available) {
|
|
setEventMessages($langs->trans('ErrorQtyExceedsAvailable', $qty_available), null, 'errors');
|
|
$error++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$error) {
|
|
// Produkt zum Stundenzettel hinzufügen mit origin='returned'
|
|
$result = $object->addProduct(
|
|
$fk_product,
|
|
$commandedet_id, // fk_commandedet
|
|
0, // fk_manager_line
|
|
0, // qty_original
|
|
$qty, // qty_done (Menge die zurückgenommen wird)
|
|
'returned', // origin (rücknahme)
|
|
$description, // description (Grund)
|
|
$freetext_label // product_label für Freitext-Produkte
|
|
);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
}
|
|
} else {
|
|
setEventMessages($langs->trans('ErrorNoProductSelected'), null, 'errors');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products');
|
|
exit;
|
|
}
|
|
|
|
// Rücknahme-Produkt löschen
|
|
if ($action == 'confirm_delete_ruecknahme' && $confirm == 'yes' && $permissiontoadd) {
|
|
$line_id = GETPOST('line_id', 'int');
|
|
$result = $object->deleteProduct($line_id);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products');
|
|
exit;
|
|
}
|
|
|
|
// Mehraufwand hinzufügen (zusätzliches Produkt nicht aus Auftrag)
|
|
if ($action == 'add_mehraufwand' && $permissiontoadd) {
|
|
$fk_product = GETPOST('mehraufwand_product', 'int');
|
|
$qty = (float)price2num(GETPOST('mehraufwand_qty', 'alpha'));
|
|
$freetext_description = GETPOST('mehraufwand_description', 'restricthtml');
|
|
$reason = GETPOST('mehraufwand_reason', 'restricthtml');
|
|
|
|
// Beschreibung:
|
|
// - Bei Katalog-Produkt: Nur der Grund (Produktname kommt aus der Produkt-Tabelle)
|
|
// - Bei Freitext: Freitext ist die Beschreibung (Grund wird nicht unterstützt)
|
|
$description = '';
|
|
if ($fk_product > 0) {
|
|
// Katalog-Produkt: Grund als Beschreibung
|
|
$description = $reason;
|
|
} else {
|
|
// Freitext-Produkt: Freitext als Beschreibung
|
|
$description = $freetext_description;
|
|
}
|
|
|
|
if ($fk_product > 0 || !empty($description)) {
|
|
// Mehraufwand wird IMMER als origin='additional' gespeichert
|
|
// (unabhängig davon, ob das Produkt im Auftrag existiert oder nicht)
|
|
// Prüfen ob bereits als Mehraufwand auf diesem Stundenzettel existiert
|
|
$existingLineId = 0;
|
|
$existingQty = 0;
|
|
if ($fk_product > 0) {
|
|
$sqlCheck = "SELECT rowid, qty_done FROM ".MAIN_DB_PREFIX."stundenzettel_product";
|
|
$sqlCheck .= " WHERE fk_stundenzettel = ".((int)$object->id);
|
|
$sqlCheck .= " AND fk_product = ".((int)$fk_product);
|
|
$sqlCheck .= " AND origin = 'additional'";
|
|
$sqlCheck .= " LIMIT 1";
|
|
} else {
|
|
$sqlCheck = "SELECT rowid, qty_done FROM ".MAIN_DB_PREFIX."stundenzettel_product";
|
|
$sqlCheck .= " WHERE fk_stundenzettel = ".((int)$object->id);
|
|
$sqlCheck .= " AND (fk_product IS NULL OR fk_product = 0)";
|
|
$sqlCheck .= " AND origin = 'additional'";
|
|
$sqlCheck .= " AND description = '".$db->escape($description)."'";
|
|
$sqlCheck .= " LIMIT 1";
|
|
}
|
|
$resqlCheck = $db->query($sqlCheck);
|
|
if ($resqlCheck && ($objCheck = $db->fetch_object($resqlCheck))) {
|
|
$existingLineId = $objCheck->rowid;
|
|
$existingQty = (float)$objCheck->qty_done;
|
|
}
|
|
|
|
if ($existingLineId > 0) {
|
|
// Existiert bereits - Menge erhöhen
|
|
$newQty = $existingQty + $qty;
|
|
$result = $object->updateProductQty($existingLineId, $newQty);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordModified'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
} else {
|
|
// Neu als Mehraufwand anlegen
|
|
$result = $object->addProduct(
|
|
$fk_product,
|
|
0, // fk_commandedet
|
|
0, // fk_manager_line
|
|
0, // qty_original
|
|
$qty, // qty_done = Menge
|
|
'additional', // origin = Mehraufwand
|
|
$description
|
|
);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
}
|
|
} else {
|
|
setEventMessages($langs->trans('ErrorNoProductSelected'), null, 'errors');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products');
|
|
exit;
|
|
}
|
|
|
|
// Mehraufwand-Produkt aktualisieren (Menge und Grund)
|
|
if ($action == 'update_mehraufwand' && $permissiontoadd) {
|
|
$line_id = GETPOST('line_id', 'int');
|
|
$qty_done = (float)price2num(GETPOST('qty_done', 'alpha'));
|
|
$reason = GETPOST('reason', 'restricthtml');
|
|
|
|
if ($line_id > 0 && $qty_done > 0) {
|
|
// Menge und Grund aktualisieren
|
|
$sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_product";
|
|
$sql .= " SET qty_done = ".((float)$qty_done);
|
|
$sql .= ", description = ".($reason !== '' ? "'".$db->escape($reason)."'" : "NULL");
|
|
$sql .= " WHERE rowid = ".((int)$line_id);
|
|
$sql .= " AND fk_stundenzettel = ".((int)$object->id);
|
|
|
|
$result = $db->query($sql);
|
|
if ($result) {
|
|
setEventMessages($langs->trans('RecordModified'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($db->lasterror(), null, 'errors');
|
|
}
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products');
|
|
exit;
|
|
}
|
|
|
|
// Mehraufwand-Produkt löschen
|
|
if ($action == 'confirm_delete_mehraufwand' && $confirm == 'yes' && $permissiontoadd) {
|
|
$line_id = GETPOST('line_id', 'int');
|
|
$result = $object->deleteProduct($line_id);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products');
|
|
exit;
|
|
}
|
|
|
|
// Produkt aus Produktliste löschen
|
|
if ($action == 'confirm_delete_product' && $confirm == 'yes' && $permissiontoadd) {
|
|
$line_id = GETPOST('line_id', 'int');
|
|
$result = $object->deleteProduct($line_id);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products');
|
|
exit;
|
|
}
|
|
|
|
// Notiz hinzufügen
|
|
if ($action == 'add_note' && $permissiontoadd) {
|
|
$note_text = GETPOST('note_text', 'restricthtml');
|
|
if (!empty($note_text)) {
|
|
$result = $object->addNote($user, $note_text);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id);
|
|
exit;
|
|
}
|
|
|
|
// Notiz-Status umschalten (abhaken/öffnen)
|
|
if ($action == 'toggle_note' && $permissiontoadd) {
|
|
$note_id = GETPOST('note_id', 'int');
|
|
$checked = GETPOST('checked', 'int');
|
|
$result = $object->updateNoteStatus($note_id, $checked);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordModified'), null, 'mesgs');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id);
|
|
exit;
|
|
}
|
|
|
|
// Notiz löschen
|
|
if ($action == 'confirm_delete_note' && $confirm == 'yes' && $permissiontoadd) {
|
|
$note_id = GETPOST('note_id', 'int');
|
|
$result = $object->deleteNote($note_id);
|
|
if ($result > 0) {
|
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
|
} else {
|
|
setEventMessages($object->error, null, 'errors');
|
|
}
|
|
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id);
|
|
exit;
|
|
}
|
|
|
|
/*
|
|
* View
|
|
*/
|
|
|
|
$form = new Form($db);
|
|
|
|
$title = $langs->trans("Stundenzettel");
|
|
if ($object->id > 0) {
|
|
$title .= ' - '.$object->ref;
|
|
}
|
|
|
|
// Linkes Menü aktivieren
|
|
$_GET['mainmenu'] = 'stundenzettel';
|
|
|
|
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-stundenzettel page-card');
|
|
|
|
// Mobile CSS einbinden
|
|
$mobileCssFile = dol_buildpath('/stundenzettel/css/stundenzettel-mobile.css', 0);
|
|
if (file_exists($mobileCssFile)) {
|
|
print '<link rel="stylesheet" type="text/css" href="'.dol_buildpath('/stundenzettel/css/stundenzettel-mobile.css', 1).'?v='.filemtime($mobileCssFile).'">';
|
|
}
|
|
|
|
// JavaScript für Mengenprüfung
|
|
print '<script type="text/javascript">
|
|
function plusQtyWithCheck(rowid, qtyOriginal, totalQtyAllStz) {
|
|
var f = document.getElementById("qty_" + rowid);
|
|
var currentQty = parseInt(f.value || 0);
|
|
var newQty = currentQty + 1;
|
|
// Gesamtmenge über alle Stundenzettel (ohne aktuelle Zeile) + neue Menge
|
|
var newTotal = (totalQtyAllStz - currentQty) + newQty;
|
|
|
|
if (qtyOriginal > 0 && newTotal > qtyOriginal) {
|
|
if (confirm("'.dol_escape_js($langs->trans("ConfirmExceedOrderQty")).'\\n\\n'.dol_escape_js($langs->trans("OrderQty")).': " + qtyOriginal + "\\n'.dol_escape_js($langs->trans("TotalUsedAllStz")).': " + newTotal + "\\n\\n'.dol_escape_js($langs->trans("ConfirmAdditionalWork")).'")) {
|
|
f.value = newQty;
|
|
document.getElementById("form_qty_" + rowid).submit();
|
|
}
|
|
} else {
|
|
f.value = newQty;
|
|
document.getElementById("form_qty_" + rowid).submit();
|
|
}
|
|
}
|
|
</script>';
|
|
|
|
// Create mode
|
|
if ($action == 'create') {
|
|
// Prüfe ob Auftrag freigegeben ist
|
|
if ($fk_commande > 0) {
|
|
$orderCheck = new Commande($db);
|
|
if ($orderCheck->fetch($fk_commande) > 0) {
|
|
$orderCheck->fetch_optionals();
|
|
$stzStatus = isset($orderCheck->array_options['options_stundenzettel_status']) ? (int)$orderCheck->array_options['options_stundenzettel_status'] : 0;
|
|
if ($stzStatus >= 1) {
|
|
setEventMessages($langs->trans('ErrorStundenzettelReleased'), null, 'errors');
|
|
header('Location: '.dol_buildpath('/stundenzettel/stundenzettel_commande.php?id='.$fk_commande.'&tab=stundenzettel&noredirect=1', 1));
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
|
|
print load_fiche_titre($langs->trans("CreateStundenzettel"), '', 'clock');
|
|
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="add">';
|
|
|
|
print dol_get_fiche_head(array(), '');
|
|
|
|
print '<table class="border centpercent tableforfieldcreate">';
|
|
|
|
// Auftrag
|
|
print '<tr><td class="titlefieldcreate fieldrequired">'.$langs->trans("Order").'</td><td>';
|
|
if ($fk_commande > 0) {
|
|
$order = new Commande($db);
|
|
$order->fetch($fk_commande);
|
|
print $order->getNomUrl(1);
|
|
print '<input type="hidden" name="fk_commande" value="'.$fk_commande.'">';
|
|
} else {
|
|
// Auftrag auswählen
|
|
$sql = "SELECT c.rowid, c.ref, s.nom as soc_name";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX."commande as c";
|
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = c.fk_soc";
|
|
$sql .= " WHERE c.entity = ".((int)$conf->entity);
|
|
$sql .= " AND c.fk_statut IN (1, 2)";
|
|
$sql .= " ORDER BY c.date_commande DESC";
|
|
$sql .= " LIMIT 100";
|
|
|
|
print '<select name="fk_commande" class="flat minwidth300">';
|
|
print '<option value="">-- '.$langs->trans("SelectOrder").' --</option>';
|
|
$resql = $db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $db->fetch_object($resql)) {
|
|
print '<option value="'.$obj->rowid.'">'.$obj->ref.' - '.$obj->soc_name.'</option>';
|
|
}
|
|
}
|
|
print '</select>';
|
|
}
|
|
print '</td></tr>';
|
|
|
|
// Datum
|
|
print '<tr><td class="fieldrequired">'.$langs->trans("Date").'</td><td>';
|
|
print $form->selectDate(dol_now(), 'date_stundenzettel', 0, 0, 0, '', 1, 1);
|
|
print '</td></tr>';
|
|
|
|
// Notiz öffentlich
|
|
print '<tr><td>'.$langs->trans("NotePublic").'</td><td>';
|
|
$doleditor = new DolEditor('note_public', '', '', 150, 'dolibarr_notes', '', false, true, getDolGlobalString('FCKEDITOR_ENABLE_NOTE_PUBLIC'), ROWS_5, '90%');
|
|
print $doleditor->Create(1);
|
|
print '</td></tr>';
|
|
|
|
// Notiz privat
|
|
print '<tr><td>'.$langs->trans("NotePrivate").'</td><td>';
|
|
$doleditor = new DolEditor('note_private', '', '', 150, 'dolibarr_notes', '', false, true, getDolGlobalString('FCKEDITOR_ENABLE_NOTE_PRIVATE'), ROWS_5, '90%');
|
|
print $doleditor->Create(1);
|
|
print '</td></tr>';
|
|
|
|
print '</table>';
|
|
|
|
print dol_get_fiche_end();
|
|
|
|
print '<div class="center">';
|
|
print '<input type="submit" class="button button-primary" value="'.$langs->trans("Create").'">';
|
|
print ' ';
|
|
print '<a class="button button-cancel" href="'.dol_buildpath('/stundenzettel/index.php', 1).'">'.$langs->trans("Cancel").'</a>';
|
|
print '</div>';
|
|
|
|
print '</form>';
|
|
}
|
|
|
|
// View/Edit mode
|
|
elseif ($object->id > 0) {
|
|
// Confirmations
|
|
if ($action == 'validate') {
|
|
print $form->formconfirm($_SERVER['PHP_SELF'].'?id='.$object->id, $langs->trans('ValidateStundenzettel'), $langs->trans('ConfirmValidate'), 'confirm_validate', '', 0, 1);
|
|
}
|
|
if ($action == 'setdraft') {
|
|
print $form->formconfirm($_SERVER['PHP_SELF'].'?id='.$object->id, $langs->trans('ReopenStundenzettel'), $langs->trans('ConfirmSetDraft'), 'confirm_setdraft', '', 0, 1);
|
|
}
|
|
if ($action == 'delete') {
|
|
print $form->formconfirm($_SERVER['PHP_SELF'].'?id='.$object->id, $langs->trans('DeleteStundenzettel'), $langs->trans('ConfirmDelete'), 'confirm_delete', '', 0, 1);
|
|
}
|
|
|
|
// Tabs
|
|
$head = stundenzettel_prepare_head($object);
|
|
print dol_get_fiche_head($head, 'card', $langs->trans("Stundenzettel"), -1, 'clock');
|
|
|
|
// Banner
|
|
$linkback = '<a href="'.dol_buildpath('/stundenzettel/list.php', 1).'">'.$langs->trans("BackToList").'</a>';
|
|
|
|
dol_banner_tab($object, 'id', $linkback, 1, 'rowid', 'ref', '', '', 0, '', '', 1);
|
|
|
|
print '<div class="fichecenter">';
|
|
print '<div class="underbanner clearboth"></div>';
|
|
|
|
// =============================================
|
|
// BEREICH: LEISTUNGEN (immer anzeigen außer bei Notizen)
|
|
// =============================================
|
|
if ($subtab != 'notes') {
|
|
// Standard-Leistung vom Kunden laden
|
|
$thirdparty = new Societe($db);
|
|
$thirdparty->fetch($object->fk_soc);
|
|
$thirdparty->fetch_optionals();
|
|
|
|
// Prüfen ob eine Leistung bearbeitet wird
|
|
$editLeistungId = GETPOST('edit_leistung', 'int');
|
|
|
|
// Bestätigung für Löschen
|
|
if ($action == 'delete_leistung') {
|
|
$leistung_id = GETPOST('leistung_id', 'int');
|
|
print $form->formconfirm($_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=leistungen&leistung_id='.$leistung_id, $langs->trans('Delete'), $langs->trans('ConfirmDeleteLeistung'), 'confirm_delete_leistung', '', 0, 1);
|
|
}
|
|
|
|
print '<h3>'.$langs->trans("Leistungen").'</h3>';
|
|
print '<div class="div-table-responsive-no-min">';
|
|
print '<table class="noborder centpercent">';
|
|
print '<tr class="liste_titre">';
|
|
print '<th>'.$langs->trans("Date").'</th>';
|
|
print '<th>'.$langs->trans("LeistungTimeStart").'</th>';
|
|
print '<th>'.$langs->trans("LeistungTimeEnd").'</th>';
|
|
print '<th class="center">'.$langs->trans("Duration").'</th>';
|
|
print '<th>'.$langs->trans("DefaultService").'</th>';
|
|
print '<th class="mobile-hide">'.$langs->trans("Description").'</th>';
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
print '<th class="center" width="40"></th>'; // Edit
|
|
print '<th class="center" width="40"></th>'; // Delete
|
|
}
|
|
print '</tr>';
|
|
|
|
if (count($object->leistungen) > 0) {
|
|
foreach ($object->leistungen as $leistung) {
|
|
// Bearbeitungsmodus für diese Zeile?
|
|
if ($editLeistungId == $leistung->rowid && $object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
// Bearbeitungszeile
|
|
print '<tr class="oddeven">';
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=leistungen">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="update_leistung">';
|
|
print '<input type="hidden" name="leistung_id" value="'.$leistung->rowid.'">';
|
|
|
|
print '<td>'.$form->selectDate($db->jdate($leistung->date_leistung), 'leistung_date', 0, 0, 0, '', 1, 0).'</td>';
|
|
print '<td><input type="time" name="time_start" class="flat" style="width:90px;" value="'.$leistung->time_start.'"></td>';
|
|
print '<td><input type="time" name="time_end" class="flat" style="width:90px;" value="'.$leistung->time_end.'"></td>';
|
|
print '<td class="center">';
|
|
if ($leistung->duration > 0) {
|
|
$hours = floor($leistung->duration / 60);
|
|
$mins = $leistung->duration % 60;
|
|
print $hours.'h '.sprintf('%02d', $mins).'min';
|
|
}
|
|
print '</td>';
|
|
// Service/Product Selection
|
|
print '<td>';
|
|
$form->select_produits($leistung->fk_product, 'fk_product', 1, 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'maxwidth200');
|
|
print '</td>';
|
|
print '<td><textarea name="leistung_description" class="flat" rows="5" style="width: 300px; resize: vertical;">'.dol_escape_htmltag($leistung->description).'</textarea></td>';
|
|
// Save Button
|
|
print '<td class="center">';
|
|
print '<button type="submit" class="button" title="'.$langs->trans("Save").'" style="border: none; background: none; cursor: pointer;"><span class="fas fa-save" style="color: #007bff;"></span></button>';
|
|
print '</td>';
|
|
// Cancel Button
|
|
print '<td class="center">';
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=leistungen" title="'.$langs->trans("Cancel").'"><span class="fas fa-times" style="color: #dc3545;"></span></a>';
|
|
print '</td>';
|
|
print '</form>';
|
|
print '</tr>';
|
|
} else {
|
|
// Normale Anzeige
|
|
print '<tr class="oddeven">';
|
|
print '<td>'.dol_print_date($db->jdate($leistung->date_leistung), 'day').'</td>';
|
|
print '<td>'.$leistung->time_start.'</td>';
|
|
print '<td>'.$leistung->time_end.'</td>';
|
|
print '<td class="center">';
|
|
if ($leistung->duration > 0) {
|
|
$hours = floor($leistung->duration / 60);
|
|
$mins = $leistung->duration % 60;
|
|
print $hours.'h '.sprintf('%02d', $mins).'min';
|
|
}
|
|
print '</td>';
|
|
// Leistungsposition anzeigen
|
|
print '<td>';
|
|
if (!empty($leistung->fk_product)) {
|
|
print dol_escape_htmltag($leistung->product_label);
|
|
} else {
|
|
print '<span class="opacitymedium">'.$langs->trans("NotSet").'</span>';
|
|
}
|
|
// Mobile: Beschreibung unter Leistungsposition anzeigen
|
|
if (!empty($leistung->description)) {
|
|
print '<div class="mobile-inline-desc"><small class="opacitymedium">'.dol_trunc(strip_tags($leistung->description), 60).'</small></div>';
|
|
}
|
|
print '</td>';
|
|
// Beschreibung (auf Mobile ausgeblendet)
|
|
print '<td class="mobile-hide">'.dol_htmlentitiesbr($leistung->description).'</td>';
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
// Edit Button
|
|
print '<td class="center">';
|
|
print '<a class="editfielda" href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=leistungen&edit_leistung='.$leistung->rowid.'" title="'.$langs->trans("Edit").'"><span class="fas fa-pencil-alt" style="color: #007bff;"></span></a>';
|
|
print '</td>';
|
|
// Delete Button
|
|
print '<td class="center">';
|
|
print '<a class="deletefielda" href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=leistungen&action=delete_leistung&leistung_id='.$leistung->rowid.'&token='.newToken().'" title="'.$langs->trans("Delete").'"><span class="fas fa-trash" style="color: #dc3545;"></span></a>';
|
|
print '</td>';
|
|
}
|
|
print '</tr>';
|
|
}
|
|
}
|
|
} else {
|
|
$colspan = ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) ? 8 : 6;
|
|
print '<tr class="oddeven"><td colspan="'.$colspan.'" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
|
|
}
|
|
|
|
// Neue Leistung hinzufügen (nur im Entwurf)
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd && empty($editLeistungId)) {
|
|
// Zeit-Eingabemodus aus Konfiguration laden
|
|
$timeInputMode = getDolGlobalString('STUNDENZETTEL_TIME_INPUT_MODE', 'dropdown');
|
|
$usePreciseTime = GETPOST('precise_time', 'int') ? true : false;
|
|
$showTextInput = ($timeInputMode == 'text') || $usePreciseTime;
|
|
|
|
print '<tr class="oddeven">';
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=leistungen" id="form_leistung">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="add_leistung">';
|
|
|
|
// Datum ist fix (Stundenzettel gilt für einen Tag)
|
|
print '<td>'.dol_print_date($object->date_stundenzettel, 'day').'</td>';
|
|
print '<input type="hidden" name="leistung_date" value="'.dol_print_date($object->date_stundenzettel, '%Y-%m-%d').'">';
|
|
|
|
// Beginn-Zeit
|
|
print '<td>';
|
|
if ($showTextInput) {
|
|
print '<input type="time" name="time_start" id="time_start_text" class="flat" style="width:90px;" value="08:00">';
|
|
} else {
|
|
print '<select name="time_start" id="time_start_select" class="flat" style="width:90px;">';
|
|
print getTimeOptions('', '08');
|
|
print '</select>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Ende-Zeit
|
|
print '<td>';
|
|
if ($showTextInput) {
|
|
print '<input type="time" name="time_end" id="time_end_text" class="flat" style="width:90px;" value="16:00">';
|
|
} else {
|
|
print '<select name="time_end" id="time_end_select" class="flat" style="width:90px;">';
|
|
print getTimeOptions('', '16');
|
|
print '</select>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Dauer-Spalte - leer lassen (wird nach Speichern berechnet)
|
|
print '<td class="center">';
|
|
print '</td>';
|
|
|
|
// Leistungsposition (Service/Dienstleistung)
|
|
print '<td>';
|
|
// Standard-Leistung vom Kunden holen falls vorhanden
|
|
$defaultServiceId = 0;
|
|
if (!empty($thirdparty->array_options['options_stundenzettel_default_service'])) {
|
|
$defaultServiceId = $thirdparty->array_options['options_stundenzettel_default_service'];
|
|
}
|
|
$form->select_produits($defaultServiceId, 'fk_product', 1, 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'maxwidth200');
|
|
print '</td>';
|
|
|
|
// Beschreibung (Desktop: in Zeile, Mobile: ausgeblendet)
|
|
print '<td class="mobile-hide">';
|
|
print '<textarea name="leistung_description" class="flat" rows="3" style="width: 200px; resize: vertical;" placeholder="'.$langs->trans("Description").'"></textarea>';
|
|
print '</td>';
|
|
|
|
// Action (colspan=2 für beide Button-Spalten)
|
|
print '<td class="center" colspan="2">';
|
|
print '<button type="submit" class="button" title="'.$langs->trans("Add").'" style="border: none; background: none; cursor: pointer;"><span class="fas fa-plus-circle" style="color: #28a745; font-size: 1.3em;"></span></button>';
|
|
print '</td>';
|
|
|
|
print '</form>';
|
|
print '</tr>';
|
|
|
|
// Mobile: Beschreibung in separater Zeile
|
|
print '<tr class="oddeven mobile-description-row">';
|
|
print '<td colspan="8">';
|
|
print '<textarea name="leistung_description" form="form_leistung" class="flat" rows="3" style="width: 100%; resize: vertical;" placeholder="'.$langs->trans("Description").'"></textarea>';
|
|
print '</td>';
|
|
print '</tr>';
|
|
}
|
|
|
|
// Summenzeile anzeigen
|
|
if (count($object->leistungen) > 0) {
|
|
$totalDuration = 0;
|
|
foreach ($object->leistungen as $l) {
|
|
$totalDuration += $l->duration;
|
|
}
|
|
if ($totalDuration > 0) {
|
|
$hours = floor($totalDuration / 60);
|
|
$mins = $totalDuration % 60;
|
|
$colspan = ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) ? 8 : 6;
|
|
print '<tr class="liste_total">';
|
|
print '<td colspan="3" class="right"><strong>'.$langs->trans("Total").'</strong></td>';
|
|
print '<td class="center"><strong>'.$hours.'h '.sprintf('%02d', $mins).'min</strong></td>';
|
|
print '<td colspan="'.($colspan - 4).'"></td>';
|
|
print '</tr>';
|
|
}
|
|
}
|
|
|
|
print '</table>';
|
|
print '</div>';
|
|
}
|
|
|
|
// =============================================
|
|
// BEREICH: PRODUKTE (immer anzeigen außer bei Notizen)
|
|
// =============================================
|
|
if ($subtab != 'notes') {
|
|
// Bestätigung für Löschen von Produkten
|
|
if ($action == 'delete_product') {
|
|
$line_id = GETPOST('line_id', 'int');
|
|
print $form->formconfirm($_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products&line_id='.$line_id, $langs->trans('Delete'), $langs->trans('ConfirmDeleteLine'), 'confirm_delete_product', '', 0, 1);
|
|
}
|
|
|
|
print '<div class="div-table-responsive-no-min" style="margin-top: 15px;">';
|
|
print '<table class="noborder centpercent">';
|
|
$colspanProducts = ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) ? 8 : 4;
|
|
print '<tr class="liste_titre"><th colspan="'.$colspanProducts.'" style="border-bottom: 2px solid #dee2e6;"><strong>'.$langs->trans("Products").'</strong> - <span class="opacitymedium" style="font-weight:normal;">'.$langs->trans("ProductsDesc").'</span></th></tr>';
|
|
print '<tr class="liste_titre">';
|
|
print '<th>'.$langs->trans("Product").'</th>';
|
|
print '<th class="right" style="width:80px;">'.$langs->trans("QtyFromOrder").'</th>';
|
|
print '<th class="center" style="width:80px;">'.$langs->trans("QtyUsed").'</th>';
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
print '<th class="center" style="width:40px;"></th>'; // Minus
|
|
print '<th class="center" style="width:40px;"></th>'; // Plus
|
|
print '<th class="center" style="width:40px;"></th>'; // Save
|
|
print '<th class="center" style="width:40px;"></th>'; // Delete
|
|
}
|
|
print '<th style="width:100px;">'.$langs->trans("Origin").'</th>';
|
|
print '</tr>';
|
|
|
|
// Nur Produkte aus Auftrag und Verbaut anzeigen (nicht Mehraufwand/Entfällt - die werden unten separat angezeigt)
|
|
$orderProducts = array();
|
|
foreach ($object->products as $prod) {
|
|
if ($prod->origin == 'order' || $prod->origin == 'added') {
|
|
$orderProducts[] = $prod;
|
|
}
|
|
}
|
|
|
|
if (count($orderProducts) > 0) {
|
|
foreach ($orderProducts as $prod) {
|
|
// Gesamtmenge über alle Stundenzettel für dieses Produkt berechnen
|
|
$totalQtyAllStz = 0;
|
|
if ($prod->fk_commandedet > 0) {
|
|
$sqlTotal = "SELECT SUM(sp.qty_done) as total FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
|
$sqlTotal .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
|
$sqlTotal .= " WHERE sp.fk_commandedet = ".((int)$prod->fk_commandedet);
|
|
$sqlTotal .= " AND s.fk_commande = ".((int)$object->fk_commande);
|
|
$resqlTotal = $db->query($sqlTotal);
|
|
if ($resqlTotal && ($objTotal = $db->fetch_object($resqlTotal))) {
|
|
$totalQtyAllStz = (float)$objTotal->total;
|
|
}
|
|
} else {
|
|
$totalQtyAllStz = (float)$prod->qty_done;
|
|
}
|
|
$isExceeded = ($prod->qty_original > 0 && $totalQtyAllStz > $prod->qty_original);
|
|
$exceededQty = $totalQtyAllStz - $prod->qty_original;
|
|
|
|
print '<tr class="oddeven">';
|
|
|
|
// Produkt
|
|
print '<td>';
|
|
if ($prod->fk_product > 0) {
|
|
print '<a href="'.DOL_URL_ROOT.'/product/card.php?id='.$prod->fk_product.'">';
|
|
print img_picto('', 'product', 'class="pictofixedwidth"');
|
|
print $prod->product_ref.' - '.$prod->product_label;
|
|
print '</a>';
|
|
} else {
|
|
// Freitext-Produkt: Beschreibung anzeigen
|
|
print img_picto('', 'generic', 'class="pictofixedwidth"');
|
|
$displayText = '';
|
|
if (!empty($prod->description)) {
|
|
$displayText = strip_tags($prod->description);
|
|
} elseif (!empty($prod->product_label)) {
|
|
$displayText = $prod->product_label;
|
|
}
|
|
if (empty($displayText)) {
|
|
$displayText = $langs->trans("FreeText");
|
|
}
|
|
if (strlen($displayText) > 80) {
|
|
$displayText = substr($displayText, 0, 77).'...';
|
|
}
|
|
print '<span class="opacitymedium">'.$displayText.'</span>';
|
|
}
|
|
// Mehraufwand-Warnung anzeigen wenn überschritten
|
|
if ($isExceeded) {
|
|
print ' <span class="badge badge-warning" title="'.$langs->trans("TotalUsedAllStz").': '.formatQty($totalQtyAllStz).'">+'.formatQty($exceededQty).' '.$langs->trans("Mehraufwand").'</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Menge aus Auftrag
|
|
print '<td class="right" style="width:80px;">'.formatQty($prod->qty_original).'</td>';
|
|
|
|
// Menge erledigt
|
|
print '<td class="center" style="width:80px;">';
|
|
// Wert formatieren (max 2 Dezimalstellen)
|
|
$qtyDoneValue = (float)$prod->qty_done;
|
|
if ($qtyDoneValue == floor($qtyDoneValue)) {
|
|
$qtyDoneFormatted = (int)$qtyDoneValue;
|
|
} else {
|
|
$qtyDoneFormatted = round($qtyDoneValue, 2);
|
|
}
|
|
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products" style="display:inline;" id="form_qty_'.$prod->rowid.'">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="update_qty">';
|
|
print '<input type="hidden" name="line_id" value="'.$prod->rowid.'">';
|
|
print '<input type="number" name="qty_done" id="qty_'.$prod->rowid.'" value="'.$qtyDoneFormatted.'" class="flat" style="width:70px; text-align:center;" min="0" step="any" onkeypress="if(event.keyCode==13){this.form.submit();return false;}">';
|
|
print '</form>';
|
|
} else {
|
|
print formatQty($prod->qty_done);
|
|
}
|
|
print '</td>';
|
|
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
// Minus Button
|
|
print '<td class="center" style="width:40px;">';
|
|
print '<a class="reposition" href="javascript:void(0);" onclick="var f=document.getElementById(\'qty_'.$prod->rowid.'\'); f.value=Math.max(0,parseInt(f.value||0)-1); document.getElementById(\'form_qty_'.$prod->rowid.'\').submit();" title="-1">';
|
|
print '<span class="fas fa-minus-circle" style="color: #dc3545;"></span>';
|
|
print '</a>';
|
|
print '</td>';
|
|
|
|
// Plus Button - mit Warnung bei Überschreitung der Auftragsmenge
|
|
// $totalQtyAllStz und $qtyOriginal wurden am Anfang der Schleife berechnet
|
|
$qtyOriginal = (float)$prod->qty_original;
|
|
print '<td class="center" style="width:40px;">';
|
|
print '<a class="reposition" href="javascript:void(0);" onclick="plusQtyWithCheck('.$prod->rowid.', '.$qtyOriginal.', '.$totalQtyAllStz.');" title="+1">';
|
|
print '<span class="fas fa-plus-circle" style="color: #28a745;"></span>';
|
|
print '</a>';
|
|
print '</td>';
|
|
|
|
// Save Button
|
|
print '<td class="center" style="width:40px;">';
|
|
print '<a class="reposition" href="#" onclick="document.getElementById(\'form_qty_'.$prod->rowid.'\').submit(); return false;" title="'.$langs->trans("Save").'">';
|
|
print '<span class="fas fa-save" style="color: #007bff;"></span>';
|
|
print '</a>';
|
|
print '</td>';
|
|
|
|
// Delete Button
|
|
print '<td class="center" style="width:40px;">';
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products&action=delete_product&line_id='.$prod->rowid.'&token='.newToken().'" title="'.$langs->trans("Delete").'">';
|
|
print '<span class="fas fa-trash" style="color: #dc3545;"></span>';
|
|
print '</a>';
|
|
print '</td>';
|
|
}
|
|
|
|
// Herkunft
|
|
print '<td style="width:100px;">';
|
|
if ($prod->origin == 'order') {
|
|
print '<span class="badge badge-info">'.$langs->trans("FromOrder").'</span>';
|
|
} else {
|
|
print '<span class="badge badge-secondary">'.$langs->trans("Added").'</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
print '</tr>';
|
|
}
|
|
} else {
|
|
$colspan = ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) ? 8 : 4;
|
|
print '<tr class="oddeven"><td colspan="'.$colspan.'" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
|
|
}
|
|
|
|
// Formular zum Hinzufügen von Produkten (nur im Entwurf)
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
print '<tr class="liste_titre">';
|
|
print '<th colspan="8">'.$langs->trans("AddProduct").'</th>';
|
|
print '</tr>';
|
|
|
|
print '<tr class="oddeven">';
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="add_product">';
|
|
|
|
// Produkt-Auswahl (colspan für Produkt + QtyFromOrder)
|
|
print '<td colspan="2">';
|
|
print $form->select_produits('', 'add_product_id', '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth300', 0, '', null, 1);
|
|
print '<br><small class="opacitymedium">'.$langs->trans("OrFreeText").':</small><br>';
|
|
print '<input type="text" name="add_product_description" class="flat minwidth300" placeholder="'.$langs->trans("FreeTextDescription").'">';
|
|
print '</td>';
|
|
|
|
// Menge
|
|
print '<td class="center" style="width:80px;">';
|
|
print '<input type="number" name="add_product_qty" class="flat" style="width:70px; text-align:center;" value="1" min="0" step="any">';
|
|
print '</td>';
|
|
|
|
// Hinzufügen-Button (colspan für Minus, Plus, Save, Delete)
|
|
print '<td class="center" colspan="4" style="width:160px;">';
|
|
print '<button type="submit" class="button" title="'.$langs->trans("Add").'" style="border: none; background: none; cursor: pointer;">';
|
|
print '<span class="fas fa-plus-circle" style="color: #28a745; font-size: 1.3em;"></span>';
|
|
print '</button>';
|
|
print '</td>';
|
|
|
|
// Origin - leer
|
|
print '<td style="width:100px;"></td>';
|
|
|
|
print '</form>';
|
|
print '</tr>';
|
|
}
|
|
|
|
print '</table>';
|
|
print '</div>';
|
|
|
|
// =============================================
|
|
// BEREICH: ENTFÄLLT (Produkte die nicht verbaut werden)
|
|
// =============================================
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
// Bestätigung für Löschen
|
|
if ($action == 'delete_entfaellt') {
|
|
$line_id = GETPOST('line_id', 'int');
|
|
print $form->formconfirm($_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products&line_id='.$line_id, $langs->trans('Delete'), $langs->trans('ConfirmDeleteEntfaellt'), 'confirm_delete_entfaellt', '', 0, 1);
|
|
}
|
|
|
|
// Zuerst bereits erfasste Entfällt-Produkte zählen
|
|
$entfaelltProducts = array();
|
|
foreach ($object->products as $prod) {
|
|
if ($prod->origin == 'omitted') {
|
|
$entfaelltProducts[] = $prod;
|
|
}
|
|
}
|
|
$hasEntfaelltProducts = (count($entfaelltProducts) > 0);
|
|
|
|
// Bereich nur anzeigen wenn Einträge vorhanden ODER Checkbox aktiviert ODER Admin-Standard
|
|
$defaultSections = explode(',', getDolGlobalString('STUNDENZETTEL_DEFAULT_SECTIONS', ''));
|
|
$showEntfaellt = $hasEntfaelltProducts || GETPOST('show_entfaellt', 'int') || in_array('entfaellt', $defaultSections);
|
|
|
|
// Checkbox zum Ein-/Ausblenden
|
|
print '<div style="margin-top: 15px; margin-bottom: 5px;">';
|
|
print '<label style="cursor: pointer;">';
|
|
print '<input type="checkbox" id="toggle_entfaellt" '.($showEntfaellt ? 'checked' : '').' onchange="toggleSection(\'entfaellt\', this.checked)">';
|
|
print ' <span class="opacitymedium">'.$langs->trans("Entfaellt").' '.$langs->trans("ShowSection").'</span>';
|
|
if ($hasEntfaelltProducts) {
|
|
print ' <span class="badge badge-secondary">'.count($entfaelltProducts).'</span>';
|
|
}
|
|
print '</label>';
|
|
print '</div>';
|
|
|
|
print '<div id="section_entfaellt" class="div-table-responsive-no-min" style="'.($showEntfaellt ? '' : 'display:none;').'">';
|
|
print '<table class="noborder centpercent">';
|
|
print '<tr class="liste_titre"><th colspan="5" style="border-bottom: 2px solid #dee2e6;"><strong>'.$langs->trans("Entfaellt").'</strong> - <span class="opacitymedium" style="font-weight:normal;">'.$langs->trans("EntfaelltDesc").'</span></th></tr>';
|
|
print '<tr class="liste_titre">';
|
|
print '<th>'.$langs->trans("Product").'</th>';
|
|
print '<th class="center" style="width:80px;">'.$langs->trans("Qty").'</th>';
|
|
print '<th class="mobile-hide" style="width:200px;">'.$langs->trans("Reason").'</th>';
|
|
print '<th class="center" style="width:40px;"></th>'; // Save
|
|
print '<th class="center" style="width:40px;"></th>'; // Delete
|
|
print '</tr>';
|
|
|
|
if ($hasEntfaelltProducts) {
|
|
foreach ($entfaelltProducts as $prod) {
|
|
print '<tr class="oddeven">';
|
|
|
|
// Produkt
|
|
print '<td>';
|
|
if ($prod->fk_product > 0) {
|
|
print '<a href="'.DOL_URL_ROOT.'/product/card.php?id='.$prod->fk_product.'">';
|
|
print img_picto('', 'product', 'class="pictofixedwidth"');
|
|
print $prod->product_ref.' - '.$prod->product_label;
|
|
print '</a>';
|
|
} else {
|
|
print img_picto('', 'generic', 'class="pictofixedwidth"');
|
|
$displayText = !empty($prod->description) ? strip_tags($prod->description) : $langs->trans("FreeText");
|
|
if (strlen($displayText) > 80) {
|
|
$displayText = substr($displayText, 0, 77).'...';
|
|
}
|
|
print '<span class="opacitymedium">'.$displayText.'</span>';
|
|
}
|
|
print ' <span class="badge badge-secondary">'.$langs->trans("Entfaellt").'</span>';
|
|
// Mobile: Grund unter Produktname anzeigen
|
|
if (!empty($prod->description)) {
|
|
print '<div class="mobile-inline-desc"><small class="opacitymedium">'.dol_trunc(strip_tags($prod->description), 50).'</small></div>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Menge
|
|
print '<td class="center" style="width:80px;">';
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products" style="display:inline;" id="form_ent_qty_'.$prod->rowid.'">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="update_qty">';
|
|
print '<input type="hidden" name="line_id" value="'.$prod->rowid.'">';
|
|
print '<input type="number" name="qty_done" value="'.formatQty($prod->qty_done).'" class="flat" style="width:70px; text-align:center;" min="0" step="any">';
|
|
print '</form>';
|
|
print '</td>';
|
|
|
|
// Grund/Beschreibung (auf Mobile ausgeblendet)
|
|
print '<td class="mobile-hide" style="width:200px;">';
|
|
if (!empty($prod->description)) {
|
|
print dol_htmlentitiesbr($prod->description);
|
|
} else {
|
|
print '<span class="opacitymedium">-</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Save
|
|
print '<td class="center" style="width:40px;">';
|
|
print '<a href="javascript:document.getElementById(\'form_ent_qty_'.$prod->rowid.'\').submit();" title="'.$langs->trans("Save").'">';
|
|
print '<span class="fas fa-save" style="color: #007bff;"></span>';
|
|
print '</a>';
|
|
print '</td>';
|
|
|
|
// Delete
|
|
print '<td class="center" style="width:40px;">';
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products&action=delete_entfaellt&line_id='.$prod->rowid.'&token='.newToken().'" title="'.$langs->trans("Delete").'">';
|
|
print '<span class="fas fa-trash" style="color: #dc3545;"></span>';
|
|
print '</a>';
|
|
print '</td>';
|
|
|
|
print '</tr>';
|
|
}
|
|
}
|
|
|
|
// Formular zum Hinzufügen - immer am Ende
|
|
// Produkte aus dem Auftrag laden für das Dropdown MIT verfügbarer Menge
|
|
$orderProducts = array();
|
|
if ($object->fk_commande > 0) {
|
|
$sqlOrderProducts = "SELECT cd.rowid, cd.fk_product, cd.qty, cd.description,";
|
|
$sqlOrderProducts .= " p.ref as product_ref, p.label as product_label,";
|
|
// Bereits verbaut (auf allen Stundenzetteln dieses Auftrags)
|
|
$sqlOrderProducts .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
|
$sqlOrderProducts .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
|
$sqlOrderProducts .= " WHERE sp.fk_commandedet = cd.rowid AND sp.origin IN ('order', 'added')), 0) as qty_used,";
|
|
// Bereits als entfällt markiert (auf allen Stundenzetteln dieses Auftrags)
|
|
$sqlOrderProducts .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2";
|
|
$sqlOrderProducts .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel";
|
|
$sqlOrderProducts .= " WHERE (sp2.fk_commandedet = cd.rowid OR (sp2.fk_product = cd.fk_product AND cd.fk_product > 0)) AND sp2.origin = 'omitted' AND s2.fk_commande = ".((int)$object->fk_commande)."), 0) as qty_omitted";
|
|
$sqlOrderProducts .= " FROM ".MAIN_DB_PREFIX."commandedet as cd";
|
|
$sqlOrderProducts .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
|
|
$sqlOrderProducts .= " WHERE cd.fk_commande = ".((int)$object->fk_commande);
|
|
$sqlOrderProducts .= " AND (cd.fk_product > 0 OR ((cd.fk_product IS NULL OR cd.fk_product = 0) AND cd.description IS NOT NULL AND cd.description != ''))";
|
|
$sqlOrderProducts .= " AND (cd.special_code IS NULL OR cd.special_code = 0)";
|
|
$sqlOrderProducts .= " ORDER BY cd.rang";
|
|
$resqlOrderProducts = $db->query($sqlOrderProducts);
|
|
if ($resqlOrderProducts) {
|
|
while ($objProd = $db->fetch_object($resqlOrderProducts)) {
|
|
// Verfügbare Menge = Auftragsmenge - verbaut - bereits entfallen
|
|
$objProd->qty_available = $objProd->qty - $objProd->qty_used - $objProd->qty_omitted;
|
|
$orderProducts[] = $objProd;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Nur anzeigen wenn Produkte mit verfügbarer Menge existieren
|
|
$hasAvailableProducts = false;
|
|
foreach ($orderProducts as $op) {
|
|
if ($op->qty_available > 0) {
|
|
$hasAvailableProducts = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
print '<tr class="liste_titre">';
|
|
print '<th colspan="5">'.$langs->trans("AddEntfaellt").'</th>';
|
|
print '</tr>';
|
|
|
|
print '<tr class="oddeven">';
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products" id="form_entfaellt">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="add_entfaellt">';
|
|
|
|
// Produkt-Auswahl - NUR Produkte aus dem Auftrag mit verfügbarer Menge
|
|
// Mehraufwand-Produkte mit verbleibender Menge laden (aus allen Stundenzetteln des Auftrags)
|
|
$mehraufwandAvailable = array();
|
|
if ($object->fk_commande > 0) {
|
|
$sqlMehraufwand = "SELECT sp.rowid, sp.fk_product, sp.qty, sp.qty_done, sp.description,";
|
|
$sqlMehraufwand .= " p.ref as product_ref, p.label as product_label";
|
|
$sqlMehraufwand .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
|
$sqlMehraufwand .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
|
$sqlMehraufwand .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = sp.fk_product";
|
|
$sqlMehraufwand .= " WHERE s.fk_commande = ".((int)$object->fk_commande);
|
|
$sqlMehraufwand .= " AND sp.origin = 'additional'";
|
|
$sqlMehraufwand .= " AND (sp.qty - sp.qty_done) > 0"; // Nur wo noch etwas verfügbar ist
|
|
$sqlMehraufwand .= " ORDER BY p.label, sp.description";
|
|
$resqlMehraufwand = $db->query($sqlMehraufwand);
|
|
if ($resqlMehraufwand) {
|
|
while ($objM = $db->fetch_object($resqlMehraufwand)) {
|
|
$objM->qty_available = $objM->qty - $objM->qty_done;
|
|
$mehraufwandAvailable[] = $objM;
|
|
}
|
|
}
|
|
}
|
|
|
|
$hasMehraufwandProducts = (count($mehraufwandAvailable) > 0);
|
|
|
|
print '<td>';
|
|
if ($hasAvailableProducts || $hasMehraufwandProducts) {
|
|
print '<select name="entfaellt_product" class="flat minwidth300" id="entfaellt_product_select" onchange="updateMaxQty(this)">';
|
|
print '<option value="" data-max="1">-- '.$langs->trans("SelectProducts").' --</option>';
|
|
|
|
// Produkte aus Auftrag
|
|
if ($hasAvailableProducts) {
|
|
print '<optgroup label="'.$langs->trans("FromOrder").'">';
|
|
foreach ($orderProducts as $op) {
|
|
if ($op->qty_available <= 0) continue;
|
|
|
|
if ($op->fk_product > 0) {
|
|
print '<option value="'.$op->fk_product.'" data-commandedet="'.$op->rowid.'" data-max="'.((int)$op->qty_available).'">';
|
|
print $op->product_ref.' - '.$op->product_label;
|
|
print ' ('.$langs->trans("QtyRemaining").': '.((int)$op->qty_available).')';
|
|
print '</option>';
|
|
} else {
|
|
$desc = strip_tags($op->description);
|
|
$descShort = (strlen($desc) > 50) ? substr($desc, 0, 47).'...' : $desc;
|
|
print '<option value="freetext_'.$op->rowid.'" data-commandedet="'.$op->rowid.'" data-description="'.dol_escape_htmltag($desc).'" data-max="'.((int)$op->qty_available).'">';
|
|
print $descShort.' ('.$langs->trans("QtyRemaining").': '.((int)$op->qty_available).')';
|
|
print '</option>';
|
|
}
|
|
}
|
|
print '</optgroup>';
|
|
}
|
|
|
|
// Mehraufwand-Produkte mit verbleibender Menge
|
|
if ($hasMehraufwandProducts) {
|
|
print '<optgroup label="'.$langs->trans("Mehraufwand").'">';
|
|
foreach ($mehraufwandAvailable as $mp) {
|
|
if ($mp->fk_product > 0) {
|
|
print '<option value="mehraufwand_'.$mp->rowid.'" data-max="'.((int)$mp->qty_available).'">';
|
|
print $mp->product_ref.' - '.$mp->product_label;
|
|
print ' ('.$langs->trans("QtyRemaining").': '.((int)$mp->qty_available).')';
|
|
print '</option>';
|
|
} else {
|
|
$desc = strip_tags($mp->description);
|
|
$descShort = (strlen($desc) > 50) ? substr($desc, 0, 47).'...' : $desc;
|
|
print '<option value="mehraufwand_'.$mp->rowid.'" data-description="'.dol_escape_htmltag($desc).'" data-max="'.((int)$mp->qty_available).'">';
|
|
print $descShort.' ('.$langs->trans("QtyRemaining").': '.((int)$mp->qty_available).')';
|
|
print '</option>';
|
|
}
|
|
}
|
|
print '</optgroup>';
|
|
}
|
|
|
|
print '</select>';
|
|
} else {
|
|
print '<small class="opacitymedium">'.$langs->trans("AllProductsUsedOrOmitted").'</small>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Menge (mit dynamischem max basierend auf Produktauswahl)
|
|
print '<td class="center" style="width:80px;">';
|
|
print '<input type="number" name="entfaellt_qty" id="entfaellt_qty_input" class="flat" style="width:70px; text-align:center;" value="1" min="0" step="any">';
|
|
print '</td>';
|
|
|
|
// Grund (Desktop: in Zeile, Mobile: ausgeblendet)
|
|
print '<td class="mobile-hide" style="width:200px;">';
|
|
print '<input type="text" name="entfaellt_description" class="flat" style="width:100%;" placeholder="'.$langs->trans("Reason").'">';
|
|
print '</td>';
|
|
|
|
// Hinzufügen-Button (colspan für beide Button-Spalten)
|
|
print '<td class="center" colspan="2" style="width:80px;">';
|
|
print '<button type="submit" class="button" title="'.$langs->trans("Add").'" style="border: none; background: none; cursor: pointer;">';
|
|
print '<span class="fas fa-plus-circle" style="color: #28a745; font-size: 1.3em;"></span>';
|
|
print '</button>';
|
|
print '</td>';
|
|
|
|
print '</form>';
|
|
print '</tr>';
|
|
|
|
// Mobile: Grund in separater Zeile
|
|
print '<tr class="oddeven mobile-description-row">';
|
|
print '<td colspan="5">';
|
|
print '<input type="text" name="entfaellt_description" form="form_entfaellt" class="flat" style="width:100%;" placeholder="'.$langs->trans("Reason").'">';
|
|
print '</td>';
|
|
print '</tr>';
|
|
|
|
// JavaScript für dynamische Max-Menge und Bereichs-Toggle
|
|
print '<script>
|
|
function updateMaxQty(selectElement) {
|
|
var selectedOption = selectElement.options[selectElement.selectedIndex];
|
|
var maxQty = parseFloat(selectedOption.getAttribute("data-max")) || 999;
|
|
var qtyInput = document.getElementById("entfaellt_qty_input");
|
|
if (qtyInput) {
|
|
qtyInput.max = maxQty;
|
|
if (parseFloat(qtyInput.value) > maxQty) {
|
|
qtyInput.value = maxQty;
|
|
}
|
|
}
|
|
}
|
|
|
|
function toggleSection(sectionName, show) {
|
|
var section = document.getElementById("section_" + sectionName);
|
|
if (section) {
|
|
section.style.display = show ? "" : "none";
|
|
}
|
|
}
|
|
</script>';
|
|
|
|
print '</table>';
|
|
print '</div>';
|
|
}
|
|
|
|
// =============================================
|
|
// BEREICH: MEHRAUFWAND (zusätzliche Produkte nicht aus Auftrag)
|
|
// =============================================
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
// Bestätigung für Löschen
|
|
if ($action == 'delete_mehraufwand') {
|
|
$line_id = GETPOST('line_id', 'int');
|
|
print $form->formconfirm($_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products&line_id='.$line_id, $langs->trans('Delete'), $langs->trans('ConfirmDeleteMehraufwand'), 'confirm_delete_mehraufwand', '', 0, 1);
|
|
}
|
|
|
|
// Zuerst bereits erfasste Mehraufwand-Produkte zählen
|
|
$mehraufwandProducts = array();
|
|
foreach ($object->products as $prod) {
|
|
if ($prod->origin == 'additional') {
|
|
$mehraufwandProducts[] = $prod;
|
|
}
|
|
}
|
|
$hasMehraufwandProducts = (count($mehraufwandProducts) > 0);
|
|
|
|
// Bereich nur anzeigen wenn Einträge vorhanden ODER Checkbox aktiviert ODER Admin-Standard
|
|
$defaultSectionsMehr = explode(',', getDolGlobalString('STUNDENZETTEL_DEFAULT_SECTIONS', ''));
|
|
$showMehraufwand = $hasMehraufwandProducts || GETPOST('show_mehraufwand', 'int') || in_array('mehraufwand', $defaultSectionsMehr);
|
|
|
|
// Checkbox zum Ein-/Ausblenden
|
|
print '<div style="margin-top: 15px; margin-bottom: 5px;">';
|
|
print '<label style="cursor: pointer;">';
|
|
print '<input type="checkbox" id="toggle_mehraufwand" '.($showMehraufwand ? 'checked' : '').' onchange="toggleSection(\'mehraufwand\', this.checked)">';
|
|
print ' <span class="opacitymedium">'.$langs->trans("Mehraufwand").' '.$langs->trans("ShowSection").'</span>';
|
|
if ($hasMehraufwandProducts) {
|
|
print ' <span class="badge badge-warning">'.count($mehraufwandProducts).'</span>';
|
|
}
|
|
print '</label>';
|
|
print '</div>';
|
|
|
|
print '<div id="section_mehraufwand" class="div-table-responsive-no-min" style="'.($showMehraufwand ? '' : 'display:none;').'">';
|
|
print '<table class="noborder centpercent">';
|
|
print '<tr class="liste_titre"><th colspan="5" style="border-bottom: 2px solid #dee2e6;"><strong>'.$langs->trans("Mehraufwand").'</strong> - <span class="opacitymedium" style="font-weight:normal;">'.$langs->trans("MehraufwandDesc").'</span></th></tr>';
|
|
print '<tr class="liste_titre">';
|
|
print '<th>'.$langs->trans("Product").'</th>';
|
|
print '<th class="center" style="width:80px;">'.$langs->trans("Qty").'</th>';
|
|
print '<th class="mobile-hide" style="width:200px;">'.$langs->trans("Reason").'</th>';
|
|
print '<th class="center" style="width:40px;"></th>'; // Save
|
|
print '<th class="center" style="width:40px;"></th>'; // Delete
|
|
print '</tr>';
|
|
|
|
if ($hasMehraufwandProducts) {
|
|
foreach ($mehraufwandProducts as $prod) {
|
|
$qty = (float)$prod->qty_done;
|
|
|
|
print '<tr class="oddeven">';
|
|
|
|
// Produkt
|
|
print '<td>';
|
|
if ($prod->fk_product > 0) {
|
|
print '<a href="'.DOL_URL_ROOT.'/product/card.php?id='.$prod->fk_product.'">';
|
|
print img_picto('', 'product', 'class="pictofixedwidth"');
|
|
print $prod->product_ref.' - '.$prod->product_label;
|
|
print '</a>';
|
|
} else {
|
|
print img_picto('', 'generic', 'class="pictofixedwidth"');
|
|
$displayText = !empty($prod->description) ? strip_tags($prod->description) : $langs->trans("FreeText");
|
|
if (strlen($displayText) > 80) {
|
|
$displayText = substr($displayText, 0, 77).'...';
|
|
}
|
|
print '<span class="opacitymedium">'.$displayText.'</span>';
|
|
}
|
|
print ' <span class="badge badge-warning">'.$langs->trans("Mehraufwand").'</span>';
|
|
// Mobile: Grund unter Produktname anzeigen
|
|
$reason = ($prod->fk_product > 0) ? $prod->description : '';
|
|
if (!empty($reason)) {
|
|
print '<div class="mobile-inline-desc"><small class="opacitymedium">'.dol_trunc(strip_tags($reason), 50).'</small></div>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Menge (qty_done) - editierbar
|
|
print '<td class="center" style="width:80px;">';
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products" style="display:inline;" id="form_ma_'.$prod->rowid.'">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="update_mehraufwand">';
|
|
print '<input type="hidden" name="line_id" value="'.$prod->rowid.'">';
|
|
print '<input type="number" name="qty_done" value="'.formatQty($qty).'" class="flat" style="width:70px; text-align:center;" min="0" step="any">';
|
|
} else {
|
|
print formatQty($qty);
|
|
}
|
|
print '</td>';
|
|
|
|
// Grund (auf Mobile ausgeblendet, nur wenn Produkt gesetzt)
|
|
print '<td class="mobile-hide" style="width:200px;">';
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
// Grund nur anzeigen/editieren wenn ein Produkt gewählt wurde
|
|
$reason = ($prod->fk_product > 0) ? $prod->description : '';
|
|
print '<input type="text" name="reason" value="'.dol_escape_htmltag($reason).'" class="flat" style="width:100%;" placeholder="'.$langs->trans("Reason").'">';
|
|
print '</form>';
|
|
} else {
|
|
if ($prod->fk_product > 0 && !empty($prod->description)) {
|
|
print dol_escape_htmltag($prod->description);
|
|
} else {
|
|
print '<span class="opacitymedium">-</span>';
|
|
}
|
|
}
|
|
print '</td>';
|
|
|
|
// Save
|
|
print '<td class="center" style="width:40px;">';
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
print '<a href="javascript:document.getElementById(\'form_ma_'.$prod->rowid.'\').submit();" title="'.$langs->trans("Save").'">';
|
|
print '<span class="fas fa-save" style="color: #007bff;"></span>';
|
|
print '</a>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Delete
|
|
print '<td class="center" style="width:40px;">';
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products&action=delete_mehraufwand&line_id='.$prod->rowid.'&token='.newToken().'" title="'.$langs->trans("Delete").'">';
|
|
print '<span class="fas fa-trash" style="color: #dc3545;"></span>';
|
|
print '</a>';
|
|
}
|
|
print '</td>';
|
|
|
|
print '</tr>';
|
|
}
|
|
}
|
|
|
|
// Formular zum Hinzufügen - immer am Ende (unter den hinzugefügten)
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
print '<tr class="liste_titre">';
|
|
print '<th colspan="5">'.$langs->trans("AddMehraufwand").'</th>';
|
|
print '</tr>';
|
|
|
|
print '<tr class="oddeven">';
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products" id="form_mehraufwand">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="add_mehraufwand">';
|
|
|
|
// Produkt-Auswahl
|
|
print '<td>';
|
|
print $form->select_produits('', 'mehraufwand_product', '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth200', 0, '', null, 1);
|
|
print '<br><small class="opacitymedium">'.$langs->trans("OrFreeText").':</small><br>';
|
|
print '<input type="text" name="mehraufwand_description" class="flat minwidth200" placeholder="'.$langs->trans("FreeTextDescription").'">';
|
|
print '</td>';
|
|
|
|
// Menge
|
|
print '<td class="center" style="width:80px;">';
|
|
print '<input type="number" name="mehraufwand_qty" class="flat" style="width:70px; text-align:center;" value="1" min="0" step="any">';
|
|
print '</td>';
|
|
|
|
// Grund (Desktop: in Zeile, Mobile: ausgeblendet)
|
|
print '<td class="mobile-hide" style="width:200px;">';
|
|
print '<input type="text" name="mehraufwand_reason" class="flat" style="width:100%;" placeholder="'.$langs->trans("Reason").'">';
|
|
print '</td>';
|
|
|
|
// Hinzufügen-Button (colspan für Save, Delete)
|
|
print '<td class="center" colspan="2">';
|
|
print '<button type="submit" class="button" title="'.$langs->trans("Add").'" style="border: none; background: none; cursor: pointer;">';
|
|
print '<span class="fas fa-plus-circle" style="color: #28a745; font-size: 1.3em;"></span>';
|
|
print '</button>';
|
|
print '</td>';
|
|
|
|
print '</form>';
|
|
print '</tr>';
|
|
|
|
// Mobile: Grund in separater Zeile
|
|
print '<tr class="oddeven mobile-description-row">';
|
|
print '<td colspan="5">';
|
|
print '<input type="text" name="mehraufwand_reason" form="form_mehraufwand" class="flat" style="width:100%;" placeholder="'.$langs->trans("Reason").'">';
|
|
print '</td>';
|
|
print '</tr>';
|
|
}
|
|
|
|
print '</table>';
|
|
print '</div>';
|
|
}
|
|
|
|
// =============================================
|
|
// BEREICH: RÜCKNAHME (bereits verbaute Produkte zurücknehmen)
|
|
// =============================================
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
// Bestätigung für Löschen
|
|
if ($action == 'delete_ruecknahme') {
|
|
$line_id = GETPOST('line_id', 'int');
|
|
print $form->formconfirm($_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products&line_id='.$line_id, $langs->trans('Delete'), $langs->trans('ConfirmDeleteRuecknahme'), 'confirm_delete_ruecknahme', '', 0, 1);
|
|
}
|
|
|
|
// Zähle bereits erfasste Rücknahme-Produkte
|
|
$ruecknahmeProducts = array();
|
|
foreach ($object->products as $prod) {
|
|
if ($prod->origin == 'returned') {
|
|
$ruecknahmeProducts[] = $prod;
|
|
}
|
|
}
|
|
$hasRuecknahmeProducts = (count($ruecknahmeProducts) > 0);
|
|
|
|
// Bereich nur anzeigen wenn Einträge vorhanden ODER Checkbox aktiviert ODER Admin-Standard
|
|
$defaultSectionsRueck = explode(',', getDolGlobalString('STUNDENZETTEL_DEFAULT_SECTIONS', ''));
|
|
$showRuecknahme = $hasRuecknahmeProducts || GETPOST('show_ruecknahme', 'int') || in_array('ruecknahme', $defaultSectionsRueck);
|
|
|
|
// Checkbox zum Ein-/Ausblenden
|
|
print '<div style="margin-top: 15px; margin-bottom: 5px;">';
|
|
print '<label style="cursor: pointer;">';
|
|
print '<input type="checkbox" id="toggle_ruecknahme" '.($showRuecknahme ? 'checked' : '').' onchange="toggleSection(\'ruecknahme\', this.checked)">';
|
|
print ' <span class="opacitymedium">'.$langs->trans("Ruecknahme").' '.$langs->trans("ShowSection").'</span>';
|
|
if ($hasRuecknahmeProducts) {
|
|
print ' <span class="badge badge-info">'.count($ruecknahmeProducts).'</span>';
|
|
}
|
|
print '</label>';
|
|
print '</div>';
|
|
|
|
print '<div id="section_ruecknahme" class="div-table-responsive-no-min" style="'.($showRuecknahme ? '' : 'display:none;').'">';
|
|
print '<table class="noborder centpercent">';
|
|
print '<tr class="liste_titre"><th colspan="5" style="border-bottom: 2px solid #dee2e6;"><strong>'.$langs->trans("Ruecknahme").'</strong> - <span class="opacitymedium" style="font-weight:normal;">'.$langs->trans("RuecknahmeDesc").'</span></th></tr>';
|
|
print '<tr class="liste_titre">';
|
|
print '<th>'.$langs->trans("Product").'</th>';
|
|
print '<th class="center" style="width:80px;">'.$langs->trans("Qty").'</th>';
|
|
print '<th class="mobile-hide" style="width:200px;">'.$langs->trans("Reason").'</th>';
|
|
print '<th class="center" style="width:40px;"></th>'; // Save
|
|
print '<th class="center" style="width:40px;"></th>'; // Delete
|
|
print '</tr>';
|
|
|
|
// Bereits erfasste Rücknahme-Produkte anzeigen
|
|
if ($hasRuecknahmeProducts) {
|
|
foreach ($ruecknahmeProducts as $prod) {
|
|
print '<tr class="oddeven">';
|
|
|
|
// Produkt
|
|
print '<td>';
|
|
if ($prod->fk_product > 0) {
|
|
print '<a href="'.DOL_URL_ROOT.'/product/card.php?id='.$prod->fk_product.'">';
|
|
print img_picto('', 'product', 'class="pictofixedwidth"');
|
|
print $prod->product_ref.' - '.$prod->product_label;
|
|
print '</a>';
|
|
} else {
|
|
print img_picto('', 'generic', 'class="pictofixedwidth"');
|
|
$displayText = !empty($prod->description) ? strip_tags($prod->description) : $langs->trans("FreeText");
|
|
if (strlen($displayText) > 80) {
|
|
$displayText = substr($displayText, 0, 77).'...';
|
|
}
|
|
print '<span class="opacitymedium">'.$displayText.'</span>';
|
|
}
|
|
print ' <span class="badge badge-danger">'.$langs->trans("Ruecknahme").'</span>';
|
|
// Mobile: Grund unter Produktname anzeigen
|
|
if (!empty($prod->description)) {
|
|
print '<div class="mobile-inline-desc"><small class="opacitymedium">'.dol_trunc(strip_tags($prod->description), 50).'</small></div>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Menge
|
|
print '<td class="center" style="width:80px;">';
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products" style="display:inline;" id="form_rn_qty_'.$prod->rowid.'">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="update_qty">';
|
|
print '<input type="hidden" name="line_id" value="'.$prod->rowid.'">';
|
|
print '<input type="number" name="qty_done" value="'.formatQty($prod->qty_done).'" class="flat" style="width:70px; text-align:center;" min="0" step="any">';
|
|
print '</form>';
|
|
print '</td>';
|
|
|
|
// Grund/Beschreibung (auf Mobile ausgeblendet)
|
|
print '<td class="mobile-hide" style="width:200px;">';
|
|
if (!empty($prod->description)) {
|
|
print dol_htmlentitiesbr($prod->description);
|
|
} else {
|
|
print '<span class="opacitymedium">-</span>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Save
|
|
print '<td class="center" style="width:40px;">';
|
|
print '<a href="javascript:document.getElementById(\'form_rn_qty_'.$prod->rowid.'\').submit();" title="'.$langs->trans("Save").'">';
|
|
print '<span class="fas fa-save" style="color: #007bff;"></span>';
|
|
print '</a>';
|
|
print '</td>';
|
|
|
|
// Delete
|
|
print '<td class="center" style="width:40px;">';
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products&action=delete_ruecknahme&line_id='.$prod->rowid.'&token='.newToken().'" title="'.$langs->trans("Delete").'">';
|
|
print '<span class="fas fa-trash" style="color: #dc3545;"></span>';
|
|
print '</a>';
|
|
print '</td>';
|
|
|
|
print '</tr>';
|
|
}
|
|
}
|
|
|
|
// Formular zum Hinzufügen - nur wenn Produkte verbaut wurden
|
|
// Produkte laden die bereits verbaut wurden (aus allen Stundenzetteln dieses Auftrags)
|
|
$deliveredProducts = array();
|
|
if ($object->fk_commande > 0) {
|
|
$sqlDelivered = "SELECT cd.rowid, cd.fk_product, cd.description,";
|
|
$sqlDelivered .= " p.ref as product_ref, p.label as product_label,";
|
|
// Bereits verbaut (auf allen Stundenzetteln dieses Auftrags)
|
|
$sqlDelivered .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
|
$sqlDelivered .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
|
$sqlDelivered .= " WHERE (sp.fk_commandedet = cd.rowid OR (sp.fk_product = cd.fk_product AND cd.fk_product > 0))";
|
|
$sqlDelivered .= " AND sp.origin IN ('order', 'added') AND s.fk_commande = ".((int)$object->fk_commande)."), 0) as qty_delivered,";
|
|
// Bereits zurückgenommen
|
|
$sqlDelivered .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2";
|
|
$sqlDelivered .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel";
|
|
$sqlDelivered .= " WHERE (sp2.fk_commandedet = cd.rowid OR (sp2.fk_product = cd.fk_product AND cd.fk_product > 0))";
|
|
$sqlDelivered .= " AND sp2.origin = 'returned' AND s2.fk_commande = ".((int)$object->fk_commande)."), 0) as qty_returned";
|
|
$sqlDelivered .= " FROM ".MAIN_DB_PREFIX."commandedet as cd";
|
|
$sqlDelivered .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
|
|
$sqlDelivered .= " WHERE cd.fk_commande = ".((int)$object->fk_commande);
|
|
$sqlDelivered .= " AND (cd.fk_product > 0 OR ((cd.fk_product IS NULL OR cd.fk_product = 0) AND cd.description IS NOT NULL AND cd.description != ''))";
|
|
$sqlDelivered .= " AND (cd.special_code IS NULL OR cd.special_code = 0)";
|
|
$sqlDelivered .= " ORDER BY cd.rang";
|
|
$resqlDelivered = $db->query($sqlDelivered);
|
|
if ($resqlDelivered) {
|
|
while ($objProd = $db->fetch_object($resqlDelivered)) {
|
|
// Verfügbare Menge = verbaut - bereits zurückgenommen
|
|
$objProd->qty_available = $objProd->qty_delivered - $objProd->qty_returned;
|
|
if ($objProd->qty_available > 0) {
|
|
$deliveredProducts[] = $objProd;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Auch verbaute Produkte ohne Auftragszeile berücksichtigen
|
|
// (origin='added' mit fk_commandedet IS NULL = in Produktliste hinzugefügt, nicht aus Auftrag)
|
|
$alreadyFoundProductIds = array();
|
|
foreach ($deliveredProducts as $dp) {
|
|
if ($dp->fk_product > 0) {
|
|
$alreadyFoundProductIds[] = (int)$dp->fk_product;
|
|
}
|
|
}
|
|
|
|
// Verbaute Produkte mit fk_product > 0, ohne Auftragszeile
|
|
$sqlAdded = "SELECT sp.fk_product, p.ref as product_ref, p.label as product_label,";
|
|
$sqlAdded .= " SUM(sp.qty_done) as qty_delivered,";
|
|
$sqlAdded .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2";
|
|
$sqlAdded .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel";
|
|
$sqlAdded .= " WHERE sp2.fk_product = sp.fk_product AND sp2.origin = 'returned'";
|
|
$sqlAdded .= " AND s2.fk_commande = ".((int)$object->fk_commande)."), 0) as qty_returned";
|
|
$sqlAdded .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
|
$sqlAdded .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
|
$sqlAdded .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = sp.fk_product";
|
|
$sqlAdded .= " WHERE sp.origin = 'added'";
|
|
$sqlAdded .= " AND (sp.fk_commandedet IS NULL OR sp.fk_commandedet = 0)";
|
|
$sqlAdded .= " AND s.fk_commande = ".((int)$object->fk_commande);
|
|
$sqlAdded .= " AND sp.fk_product > 0";
|
|
if (!empty($alreadyFoundProductIds)) {
|
|
$sqlAdded .= " AND sp.fk_product NOT IN (".implode(',', $alreadyFoundProductIds).")";
|
|
}
|
|
$sqlAdded .= " GROUP BY sp.fk_product, p.ref, p.label";
|
|
$resqlAdded = $db->query($sqlAdded);
|
|
if ($resqlAdded) {
|
|
while ($objAdd = $db->fetch_object($resqlAdded)) {
|
|
$objAdd->qty_available = $objAdd->qty_delivered - $objAdd->qty_returned;
|
|
$objAdd->rowid = 0;
|
|
$objAdd->description = '';
|
|
if ($objAdd->qty_available > 0) {
|
|
$deliveredProducts[] = $objAdd;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verbaute Freitext-Produkte ohne Auftragszeile (fk_product = 0, fk_commandedet = NULL)
|
|
$sqlAddedFree = "SELECT sp.rowid as sp_rowid, sp.description,";
|
|
$sqlAddedFree .= " sp.qty_done as qty_delivered, 0 as qty_returned, 0 as fk_product,";
|
|
$sqlAddedFree .= " '' as product_ref, '' as product_label";
|
|
$sqlAddedFree .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
|
$sqlAddedFree .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
|
$sqlAddedFree .= " WHERE sp.origin = 'added'";
|
|
$sqlAddedFree .= " AND (sp.fk_commandedet IS NULL OR sp.fk_commandedet = 0)";
|
|
$sqlAddedFree .= " AND s.fk_commande = ".((int)$object->fk_commande);
|
|
$sqlAddedFree .= " AND (sp.fk_product IS NULL OR sp.fk_product = 0)";
|
|
$sqlAddedFree .= " AND sp.description IS NOT NULL AND sp.description != ''";
|
|
$resqlAddedFree = $db->query($sqlAddedFree);
|
|
if ($resqlAddedFree) {
|
|
while ($objFree = $db->fetch_object($resqlAddedFree)) {
|
|
$objFree->qty_available = $objFree->qty_delivered;
|
|
$objFree->rowid = 0;
|
|
$objFree->fk_product = 0;
|
|
$objFree->is_mehraufwand_freetext = true;
|
|
if ($objFree->qty_available > 0) {
|
|
$deliveredProducts[] = $objFree;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$hasDeliveredProducts = (count($deliveredProducts) > 0);
|
|
|
|
print '<tr class="liste_titre">';
|
|
print '<th colspan="5">'.$langs->trans("AddRuecknahme").'</th>';
|
|
print '</tr>';
|
|
|
|
print '<tr class="oddeven">';
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=products" id="form_ruecknahme">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="add_ruecknahme">';
|
|
|
|
// Produkt-Auswahl - NUR Produkte die bereits verbaut wurden
|
|
print '<td>';
|
|
if ($hasDeliveredProducts) {
|
|
print '<select name="ruecknahme_product" class="flat minwidth300" id="ruecknahme_product_select" onchange="updateMaxQtyRuecknahme(this)">';
|
|
print '<option value="" data-max="1">-- '.$langs->trans("SelectProducts").' --</option>';
|
|
|
|
foreach ($deliveredProducts as $dp) {
|
|
if ($dp->fk_product > 0) {
|
|
print '<option value="'.$dp->fk_product.'" data-commandedet="'.($dp->rowid ?? 0).'" data-max="'.formatQty($dp->qty_available).'">';
|
|
print $dp->product_ref.' - '.$dp->product_label;
|
|
print ' ('.$langs->trans("QtyDelivered").': '.formatQty($dp->qty_available).')';
|
|
print '</option>';
|
|
} elseif (!empty($dp->is_mehraufwand_freetext)) {
|
|
// Freitext-Mehraufwand
|
|
$desc = strip_tags($dp->description);
|
|
$descShort = (strlen($desc) > 50) ? substr($desc, 0, 47).'...' : $desc;
|
|
print '<option value="mehraufwand_'.$dp->sp_rowid.'" data-description="'.dol_escape_htmltag($desc).'" data-max="'.formatQty($dp->qty_available).'">';
|
|
print $descShort.' ('.$langs->trans("QtyDelivered").': '.formatQty($dp->qty_available).')';
|
|
print '</option>';
|
|
} else {
|
|
// Freitext aus Auftrag
|
|
$desc = strip_tags($dp->description);
|
|
$descShort = (strlen($desc) > 50) ? substr($desc, 0, 47).'...' : $desc;
|
|
print '<option value="freetext_'.$dp->rowid.'" data-commandedet="'.$dp->rowid.'" data-description="'.dol_escape_htmltag($desc).'" data-max="'.formatQty($dp->qty_available).'">';
|
|
print $descShort.' ('.$langs->trans("QtyDelivered").': '.formatQty($dp->qty_available).')';
|
|
print '</option>';
|
|
}
|
|
}
|
|
|
|
print '</select>';
|
|
} else {
|
|
print '<small class="opacitymedium">'.$langs->trans("NoProductsDelivered").'</small>';
|
|
}
|
|
print '</td>';
|
|
|
|
// Menge (mit dynamischem max basierend auf Produktauswahl)
|
|
print '<td class="center" style="width:80px;">';
|
|
print '<input type="number" name="ruecknahme_qty" id="ruecknahme_qty_input" class="flat" style="width:70px; text-align:center;" value="1" min="0" step="any">';
|
|
print '</td>';
|
|
|
|
// Grund (Desktop: in Zeile, Mobile: ausgeblendet)
|
|
print '<td class="mobile-hide" style="width:200px;">';
|
|
print '<input type="text" name="ruecknahme_description" class="flat" style="width:100%;" placeholder="'.$langs->trans("Reason").'">';
|
|
print '</td>';
|
|
|
|
// Hinzufügen-Button (colspan für beide Button-Spalten)
|
|
print '<td class="center" colspan="2" style="width:80px;">';
|
|
print '<button type="submit" class="button" title="'.$langs->trans("Add").'" style="border: none; background: none; cursor: pointer;">';
|
|
print '<span class="fas fa-plus-circle" style="color: #28a745; font-size: 1.3em;"></span>';
|
|
print '</button>';
|
|
print '</td>';
|
|
|
|
print '</form>';
|
|
print '</tr>';
|
|
|
|
// Mobile: Grund in separater Zeile
|
|
print '<tr class="oddeven mobile-description-row">';
|
|
print '<td colspan="5">';
|
|
print '<input type="text" name="ruecknahme_description" form="form_ruecknahme" class="flat" style="width:100%;" placeholder="'.$langs->trans("Reason").'">';
|
|
print '</td>';
|
|
print '</tr>';
|
|
|
|
// JavaScript für dynamische Max-Menge
|
|
print '<script>
|
|
function updateMaxQtyRuecknahme(selectElement) {
|
|
var selectedOption = selectElement.options[selectElement.selectedIndex];
|
|
var maxQty = parseFloat(selectedOption.getAttribute("data-max")) || 999;
|
|
var qtyInput = document.getElementById("ruecknahme_qty_input");
|
|
if (qtyInput) {
|
|
qtyInput.max = maxQty;
|
|
if (parseFloat(qtyInput.value) > maxQty) {
|
|
qtyInput.value = maxQty;
|
|
}
|
|
}
|
|
}
|
|
|
|
function toggleSection(sectionName, show) {
|
|
var section = document.getElementById("section_" + sectionName);
|
|
if (section) {
|
|
section.style.display = show ? "" : "none";
|
|
}
|
|
}
|
|
</script>';
|
|
|
|
print '</table>';
|
|
print '</div>';
|
|
}
|
|
|
|
// =============================================
|
|
// BEREICH: MERKZETTEL (abhakbare Notizen)
|
|
// =============================================
|
|
// Bestätigung für Löschen
|
|
if ($action == 'delete_note') {
|
|
$note_id = GETPOST('note_id', 'int');
|
|
print $form->formconfirm($_SERVER['PHP_SELF'].'?id='.$object->id.'¬e_id='.$note_id, $langs->trans('Delete'), $langs->trans('ConfirmDeleteNote'), 'confirm_delete_note', '', 0, 1);
|
|
}
|
|
|
|
// Prüfen ob Notizen vorhanden
|
|
$hasNotes = (count($object->notes) > 0);
|
|
|
|
// Bereich nur anzeigen wenn Einträge vorhanden ODER Checkbox aktiviert ODER Admin-Standard
|
|
$defaultSectionsMerk = explode(',', getDolGlobalString('STUNDENZETTEL_DEFAULT_SECTIONS', ''));
|
|
$showMerkzettel = $hasNotes || GETPOST('show_merkzettel', 'int') || in_array('merkzettel', $defaultSectionsMerk);
|
|
|
|
// Checkbox zum Ein-/Ausblenden
|
|
print '<div style="margin-top: 15px; margin-bottom: 5px;">';
|
|
print '<label style="cursor: pointer;">';
|
|
print '<input type="checkbox" id="toggle_merkzettel" '.($showMerkzettel ? 'checked' : '').' onchange="toggleSection(\'merkzettel\', this.checked)">';
|
|
print ' <span class="opacitymedium">'.$langs->trans("NotesMemo").' '.$langs->trans("ShowSection").'</span>';
|
|
if ($hasNotes) {
|
|
print ' <span class="badge badge-info">'.count($object->notes).'</span>';
|
|
}
|
|
print '</label>';
|
|
print '</div>';
|
|
|
|
print '<div id="section_merkzettel" class="div-table-responsive-no-min" style="'.($showMerkzettel ? '' : 'display:none;').'">';
|
|
print '<table class="noborder centpercent">';
|
|
print '<tr class="liste_titre"><th colspan="4" style="border-bottom: 2px solid #dee2e6;"><strong>'.$langs->trans("NotesMemo").'</strong> - <span class="opacitymedium" style="font-weight:normal;">'.$langs->trans("NotesForNextVisit").'</span></th></tr>';
|
|
|
|
// Vorhandene Notizen anzeigen
|
|
if (count($object->notes) > 0) {
|
|
foreach ($object->notes as $note) {
|
|
$isChecked = ($note->checked == 1);
|
|
print '<tr class="oddeven">';
|
|
|
|
// Checkbox
|
|
print '<td style="width:30px;" class="center">';
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
$newChecked = $isChecked ? 0 : 1;
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&action=toggle_note¬e_id='.$note->rowid.'&checked='.$newChecked.'&token='.newToken().'">';
|
|
if ($isChecked) {
|
|
print '<span class="fas fa-check-square" style="color: #28a745; font-size: 1.2em;"></span>';
|
|
} else {
|
|
print '<span class="far fa-square" style="color: #6c757d; font-size: 1.2em;"></span>';
|
|
}
|
|
print '</a>';
|
|
} else {
|
|
if ($isChecked) {
|
|
print '<span class="fas fa-check-square" style="color: #28a745; font-size: 1.2em;"></span>';
|
|
} else {
|
|
print '<span class="far fa-square" style="color: #6c757d; font-size: 1.2em;"></span>';
|
|
}
|
|
}
|
|
print '</td>';
|
|
|
|
// Notiz-Text
|
|
print '<td'.($isChecked ? ' style="text-decoration: line-through; color: #6c757d;"' : '').'>';
|
|
print dol_htmlentitiesbr($note->note);
|
|
print '</td>';
|
|
|
|
// Datum
|
|
print '<td style="width:120px;" class="opacitymedium">';
|
|
print dol_print_date($db->jdate($note->datec), 'dayhour');
|
|
print '</td>';
|
|
|
|
// Löschen
|
|
print '<td style="width:40px;" class="center">';
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&action=delete_note¬e_id='.$note->rowid.'&token='.newToken().'" title="'.$langs->trans("Delete").'">';
|
|
print '<span class="fas fa-trash" style="color: #dc3545;"></span>';
|
|
print '</a>';
|
|
}
|
|
print '</td>';
|
|
|
|
print '</tr>';
|
|
}
|
|
} else {
|
|
print '<tr class="oddeven"><td colspan="4" class="opacitymedium">'.$langs->trans("NoNotesYet").'</td></tr>';
|
|
}
|
|
|
|
// Formular zum Hinzufügen (nur im Entwurf)
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
print '<tr class="oddeven">';
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="add_note">';
|
|
|
|
// Leere Checkbox-Spalte
|
|
print '<td style="width:30px;"></td>';
|
|
|
|
// Notiz-Eingabe
|
|
print '<td>';
|
|
print '<input type="text" name="note_text" class="flat minwidth400" placeholder="'.$langs->trans("AddNote").'...">';
|
|
print '</td>';
|
|
|
|
// Leere Datum-Spalte
|
|
print '<td style="width:120px;"></td>';
|
|
|
|
// Hinzufügen-Button
|
|
print '<td style="width:40px;" class="center">';
|
|
print '<button type="submit" class="button" title="'.$langs->trans("Add").'" style="border: none; background: none; cursor: pointer;">';
|
|
print '<span class="fas fa-plus-circle" style="color: #28a745; font-size: 1.3em;"></span>';
|
|
print '</button>';
|
|
print '</td>';
|
|
|
|
print '</form>';
|
|
print '</tr>';
|
|
}
|
|
|
|
print '</table>';
|
|
print '</div>';
|
|
}
|
|
|
|
// =============================================
|
|
// TAB: NOTIZEN
|
|
// =============================================
|
|
if ($subtab == 'notes') {
|
|
if ($action == 'edit' && $permissiontoadd) {
|
|
// Bearbeitungsmodus
|
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=notes">';
|
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
|
print '<input type="hidden" name="action" value="update">';
|
|
print '<input type="hidden" name="date_stundenzettelday" value="'.dol_print_date($object->date_stundenzettel, '%d').'">';
|
|
print '<input type="hidden" name="date_stundenzettelmonth" value="'.dol_print_date($object->date_stundenzettel, '%m').'">';
|
|
print '<input type="hidden" name="date_stundenzettleyear" value="'.dol_print_date($object->date_stundenzettel, '%Y').'">';
|
|
|
|
print '<table class="border centpercent">';
|
|
|
|
// Notiz öffentlich (für nächsten Termin)
|
|
print '<tr><td class="titlefield tdtop">'.$langs->trans("NotePublic").'<br><small class="opacitymedium">('.$langs->trans("NotesForNextVisit").')</small></td><td>';
|
|
$doleditor = new DolEditor('note_public', $object->note_public, '', 200, 'dolibarr_notes', '', false, true, getDolGlobalString('FCKEDITOR_ENABLE_NOTE_PUBLIC'), ROWS_5, '90%');
|
|
print $doleditor->Create(1);
|
|
print '</td></tr>';
|
|
|
|
// Notiz privat
|
|
print '<tr><td class="tdtop">'.$langs->trans("NotePrivate").'</td><td>';
|
|
$doleditor = new DolEditor('note_private', $object->note_private, '', 200, 'dolibarr_notes', '', false, true, getDolGlobalString('FCKEDITOR_ENABLE_NOTE_PRIVATE'), ROWS_5, '90%');
|
|
print $doleditor->Create(1);
|
|
print '</td></tr>';
|
|
|
|
print '</table>';
|
|
|
|
print '<div class="center" style="margin-top:15px;">';
|
|
print '<input type="submit" class="button button-primary" value="'.$langs->trans("Save").'">';
|
|
print ' ';
|
|
print '<a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=notes">'.$langs->trans("Cancel").'</a>';
|
|
print '</div>';
|
|
|
|
print '</form>';
|
|
} else {
|
|
// Ansicht
|
|
print '<table class="border centpercent">';
|
|
|
|
// Notiz öffentlich (für nächsten Termin)
|
|
print '<tr><td class="titlefield tdtop">'.$langs->trans("NotePublic").'<br><small class="opacitymedium">('.$langs->trans("NotesForNextVisit").')</small></td><td>';
|
|
if (!empty($object->note_public)) {
|
|
print dol_htmlentitiesbr($object->note_public);
|
|
} else {
|
|
print '<span class="opacitymedium">-</span>';
|
|
}
|
|
print '</td></tr>';
|
|
|
|
// Notiz privat
|
|
print '<tr><td class="tdtop">'.$langs->trans("NotePrivate").'</td><td>';
|
|
if (!empty($object->note_private)) {
|
|
print dol_htmlentitiesbr($object->note_private);
|
|
} else {
|
|
print '<span class="opacitymedium">-</span>';
|
|
}
|
|
print '</td></tr>';
|
|
|
|
print '</table>';
|
|
|
|
// Bearbeiten-Button
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
|
print '<div class="center" style="margin-top:15px;">';
|
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&tab=notes&action=edit">';
|
|
print img_picto('', 'edit', 'class="pictofixedwidth"').$langs->trans("Modify");
|
|
print '</a>';
|
|
print '</div>';
|
|
}
|
|
}
|
|
}
|
|
|
|
print '</div>'; // fichecenter
|
|
|
|
print dol_get_fiche_end();
|
|
|
|
// Buttons (immer anzeigen)
|
|
print '<div class="tabsAction">';
|
|
|
|
if ($object->status == Stundenzettel::STATUS_DRAFT) {
|
|
if ($permissiontovalidate) {
|
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&action=validate">'.$langs->trans("Validate").'</a>';
|
|
}
|
|
if ($permissiontodeleteobj) {
|
|
print '<a class="butActionDelete" href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&action=delete">'.$langs->trans("Delete").'</a>';
|
|
}
|
|
}
|
|
|
|
if ($object->status == Stundenzettel::STATUS_VALIDATED) {
|
|
if ($permissiontoadd) {
|
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?id='.$object->id.'&action=setdraft">'.$langs->trans("SetToDraft").'</a>';
|
|
}
|
|
}
|
|
|
|
print '</div>';
|
|
}
|
|
|
|
llxFooter();
|
|
$db->close();
|