commit 9627e4fea4cfa187d2d0cb4167db0f429c21e36b Author: data Date: Sat Feb 7 21:14:51 2026 +0100 V 1.1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec05cc7 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# Stundenzettel Modul für Dolibarr + +**Version:** 1.1.0 +**Autor:** Data IT Solution +**Kompatibilität:** Dolibarr 16.0+ +**Lizenz:** GPL v3 + +## Beschreibung + +Das Stundenzettel-Modul ermöglicht die Verwaltung von Stundenzetteln für Kundenaufträge in Dolibarr. Es bietet eine umfassende Dokumentation von Arbeitszeiten, verbrauchten Materialien, Mehraufwand und Notizen. + +## Funktionen + +### Kernfunktionen + +- **Stundenzettel-Verwaltung**: Erstellen, Bearbeiten und Löschen von Stundenzetteln pro Auftrag und Datum +- **Leistungserfassung**: Zeiterfassung mit Start-/Endzeit und automatischer Dauerberechnung +- **Zeitüberlappungsprüfung**: Verhindert doppelte Zeitbuchungen auf demselben Stundenzettel +- **Produktverfolgung**: Dokumentation von verbrauchten Materialien aus dem Auftrag +- **Mehraufwand**: Erfassung zusätzlicher Produkte/Dienstleistungen, die nicht im Auftrag waren +- **Entfällt-Markierung**: Kennzeichnung von Produkten, die nicht verbaut werden müssen +- **Notizen**: Merkzettel und Notizen für den nächsten Termin + +### Freigabe & Rechnungsstellung + +- **Stundenzettel-Freigabe**: Sperren von Stundenzetteln nach Fertigstellung +- **Rechnungsübernahme**: Automatische Übernahme aller Produkte und Leistungen in eine Rechnung +- **Stunden-Modus**: Wahlweise Übernahme als Gesamtstunden oder pro Tag + +### Integration + +- **SubtotalTitle-Integration**: Unterstützung für Produktgruppen aus dem SubtotalTitle-Modul +- **Auftragsintegration**: Direkter Zugriff auf Stundenzettel über Aufträge +- **Kundenintegration**: Standard-Leistung pro Kunde konfigurierbar + +## Installation + +1. Modul-Ordner nach `/custom/stundenzettel/` kopieren +2. In Dolibarr einloggen +3. Unter **Einstellungen > Module/Anwendungen** das Modul "Stundenzettel" aktivieren +4. Das Modul erstellt automatisch: + - Erforderliche Datenbanktabellen + - Extrafeld "Auftragsbeschreibung" für Aufträge + - Extrafeld "Standard-Leistung" für Kunden + - Datenbank-View für Dienstleistungen + +## Konfiguration + +Die Moduleinstellungen finden Sie unter **Einstellungen > Module > Stundenzettel > Zahnrad-Symbol**: + +| Einstellung | Beschreibung | +|-------------|--------------| +| **Zeiteingabe-Modus** | Dropdown (15-Minuten-Takt) oder Freitext (exakte Uhrzeit) | +| **Standard-Filter** | Welcher Filter in der Produktliste standardmäßig angezeigt wird | +| **Standard-Datum** | Aktuelles Datum oder Datum des letzten offenen Stundenzettels | +| **Stunden-Übernahme** | Gesamtstunden auf einer Zeile oder pro Tag eine Zeile | + +### Standard-Leistung beim Kunden + +Sie können beim Kunden (unter **Kunden > Kundenkarte**) eine Standard-Leistung (Dienstleistung) hinterlegen. Diese wird dann bei allen Stundenzetteln für diesen Kunden angezeigt und kann für die Rechnungsstellung verwendet werden. + +## Datenbanktabellen + +| Tabelle | Beschreibung | +|---------|--------------| +| `llx_stundenzettel` | Haupttabelle für Stundenzettel | +| `llx_stundenzettel_leistung` | Leistungen/Arbeitszeiten | +| `llx_stundenzettel_product` | Verbrauchte Produkte | +| `llx_stundenzettel_tracking` | Gesamtübersicht Mengen pro Auftrag | +| `llx_stundenzettel_note` | Notizen und Merkzettel | +| `llx_product_services` | View für Dienstleistungen (Extrafeld-Filter) | + +## Extrafelder + +| Feld | Element | Beschreibung | +|------|---------|--------------| +| `auftragsbeschreibung` | Auftrag | Zusätzliche Beschreibung für den Auftrag | +| `stundenzettel_status` | Auftrag | Status der Stundenzettel (0=Offen, 1=Freigegeben, 2=Abgerechnet) | +| `stundenzettel_default_service` | Kunde | Standard-Dienstleistung für Stundenzettel | + +## Berechtigungen + +| Berechtigung | Beschreibung | +|--------------|--------------| +| Lesen | Stundenzettel anzeigen | +| Erstellen/Bearbeiten | Stundenzettel erstellen und bearbeiten | +| Freigeben | Stundenzettel freigeben/sperren | +| Löschen | Stundenzettel löschen | + +## Workflow + +1. **Stundenzettel erstellen**: Über Auftrag > Stundenzettel-Tab +2. **Leistungen erfassen**: Arbeitszeiten mit Start-/Endzeit dokumentieren +3. **Produkte dokumentieren**: Verbrauchte Materialien aus dem Auftrag erfassen +4. **Mehraufwand hinzufügen**: Zusätzliche Produkte bei Bedarf +5. **Notizen erstellen**: Merkzettel für nächsten Termin +6. **Stundenzettel freigeben**: Nach Fertigstellung sperren +7. **In Rechnung übernehmen**: Automatische Rechnungserstellung + +## Changelog + +### Version 1.1.0 +- Standard-Leistung beim Kunden hinterlegen (nur Dienstleistungen auswählbar) +- Stunden-Übernahme Modus (Gesamt oder pro Tag) +- Zeitüberlappungsprüfung für Leistungen +- Verbessertes Setup-Seitenlayout +- Datenbank-View für Dienstleistungs-Filter + +### Version 1.0.0 +- Initiale Version +- Grundlegende Stundenzettel-Verwaltung +- Leistungs- und Produkterfassung +- Mehraufwand und Entfällt-Funktionen +- Notizen-System +- Freigabe und Rechnungsübernahme +- SubtotalTitle-Integration + +## Support + +Bei Fragen oder Problemen wenden Sie sich an: +**Data IT Solution** +E-Mail: data@data-it-solution.de +Web: https://data-it-solution.de + +## Lizenz + +Dieses Modul steht unter der GNU General Public License v3. Siehe LICENSE-Datei für Details. diff --git a/admin/setup.php b/admin/setup.php new file mode 100644 index 0000000..c8b0ccd --- /dev/null +++ b/admin/setup.php @@ -0,0 +1,184 @@ + + * + * Stundenzettel - Modulkonfiguration + */ + +// Load Dolibarr environment +$res = 0; +// Von /custom/stundenzettel/admin/ zu /main.inc.php = 3 Ebenen hoch +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.'/core/lib/admin.lib.php'; + +// Load translation files +$langs->loadLangs(array("admin", "stundenzettel@stundenzettel")); + +// Access control +if (!$user->admin) { + accessforbidden(); +} + +$action = GETPOST('action', 'aZ09'); + +/* + * Actions + */ + +if ($action == 'setTIME_INPUT_MODE') { + $value = GETPOST('value', 'alpha'); + if (dolibarr_set_const($db, 'STUNDENZETTEL_TIME_INPUT_MODE', $value, 'chaine', 0, '', $conf->entity) > 0) { + setEventMessages($langs->trans("SetupSaved"), null, 'mesgs'); + } else { + setEventMessages($langs->trans("Error"), null, 'errors'); + } + header("Location: ".$_SERVER["PHP_SELF"]); + exit; +} + +if ($action == 'setDEFAULT_FILTER') { + $value = GETPOST('filter_value', 'alpha'); + if (dolibarr_set_const($db, 'STUNDENZETTEL_DEFAULT_FILTER', $value, 'chaine', 0, '', $conf->entity) > 0) { + setEventMessages($langs->trans("SetupSaved"), null, 'mesgs'); + } else { + setEventMessages($langs->trans("Error"), null, 'errors'); + } + header("Location: ".$_SERVER["PHP_SELF"]); + exit; +} + +if ($action == 'setDEFAULT_DATE') { + $value = GETPOST('date_value', 'alpha'); + if (dolibarr_set_const($db, 'STUNDENZETTEL_DEFAULT_DATE', $value, 'chaine', 0, '', $conf->entity) > 0) { + setEventMessages($langs->trans("SetupSaved"), null, 'mesgs'); + } else { + setEventMessages($langs->trans("Error"), null, 'errors'); + } + header("Location: ".$_SERVER["PHP_SELF"]); + exit; +} + +if ($action == 'setINVOICE_HOURS_MODE') { + $value = GETPOST('hours_mode_value', 'alpha'); + if (dolibarr_set_const($db, 'STUNDENZETTEL_INVOICE_HOURS_MODE', $value, 'chaine', 0, '', $conf->entity) > 0) { + setEventMessages($langs->trans("SetupSaved"), null, 'mesgs'); + } else { + setEventMessages($langs->trans("Error"), null, 'errors'); + } + header("Location: ".$_SERVER["PHP_SELF"]); + exit; +} + +/* + * View + */ + +$page_name = "StundenzettelSetup"; +llxHeader('', $langs->trans($page_name), ''); + +// Subheader +$linkback = ''.$langs->trans("BackToModuleList").''; +print load_fiche_titre($langs->trans($page_name), $linkback, 'title_setup'); + +// Configuration header +$head = array(); +$h = 0; +$head[$h][0] = $_SERVER["PHP_SELF"]; +$head[$h][1] = $langs->trans("Settings"); +$head[$h][2] = 'settings'; +$h++; + +print dol_get_fiche_head($head, 'settings', $langs->trans("Stundenzettel"), -1, 'clock'); + +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; + +// Zeiteingabe-Modus +$currentMode = getDolGlobalString('STUNDENZETTEL_TIME_INPUT_MODE', 'dropdown'); +print ''; +print ''; +print ''; +print ''; +print ''; + +// Standard-Filter +$currentFilter = getDolGlobalString('STUNDENZETTEL_DEFAULT_FILTER', 'open'); +print ''; +print ''; +print ''; +print ''; +print ''; + +// Standard-Datum für Stundenzettel +$currentDate = getDolGlobalString('STUNDENZETTEL_DEFAULT_DATE', 'today'); +print ''; +print ''; +print ''; +print ''; +print ''; + +// Stunden-Übernahme Modus +$currentHoursMode = getDolGlobalString('STUNDENZETTEL_INVOICE_HOURS_MODE', 'total'); +print ''; +print ''; +print ''; +print ''; +print ''; + +print '
'.$langs->trans("Parameter").''.$langs->trans("Value").'
'.$langs->trans("TimeInputMode").''; +print '
'; +print ''; +print ''; +print ''; +print '
'; +print ''; +print ''; +print '
'.$langs->trans("DefaultFilter").'
'.$langs->trans("DefaultFilterDesc").'
'; +print '
'; +print ''; +print ''; +print ''; +print '
'; +print ''; +print ''; +print '
'.$langs->trans("DefaultDate").'
'.$langs->trans("DefaultDateDesc").'
'; +print '
'; +print ''; +print ''; +print ''; +print '
'; +print ''; +print ''; +print '
'.$langs->trans("InvoiceHoursMode").'
'.$langs->trans("SelectInvoiceHoursMode").'
'; +print '
'; +print ''; +print ''; +print ''; +print '
'; +print ''; +print ''; +print '
'; + +print dol_get_fiche_end(); + +llxFooter(); +$db->close(); diff --git a/ajax/add_leistung.php b/ajax/add_leistung.php new file mode 100644 index 0000000..8926fdf --- /dev/null +++ b/ajax/add_leistung.php @@ -0,0 +1,61 @@ + + * + * AJAX: Leistung hinzufügen + */ + +if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1'); +if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1'); +if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1'); +if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1'); + +// 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"); + +dol_include_once('/stundenzettel/class/stundenzettel.class.php'); + +header('Content-Type: application/json'); + +// Security check +if (!$user->hasRight('stundenzettel', 'write')) { + echo json_encode(array('success' => false, 'error' => 'Permission denied')); + exit; +} + +$stundenzettel_id = GETPOST('stundenzettel_id', 'int'); +$date = GETPOST('date', 'alpha'); +$time_start = GETPOST('time_start', 'alpha'); +$time_end = GETPOST('time_end', 'alpha'); +$description = GETPOST('description', 'restricthtml'); + +if (empty($stundenzettel_id)) { + echo json_encode(array('success' => false, 'error' => 'Missing stundenzettel_id')); + exit; +} + +$stundenzettel = new Stundenzettel($db); +if ($stundenzettel->fetch($stundenzettel_id) <= 0) { + echo json_encode(array('success' => false, 'error' => 'Stundenzettel not found')); + exit; +} + +// Nur im Entwurf bearbeitbar +if ($stundenzettel->status != Stundenzettel::STATUS_DRAFT) { + echo json_encode(array('success' => false, 'error' => 'Stundenzettel is not in draft status')); + exit; +} + +$result = $stundenzettel->addLeistung($user, $date, $time_start, $time_end, $description); + +if ($result > 0) { + echo json_encode(array( + 'success' => true, + 'leistung_id' => $result + )); +} else { + echo json_encode(array('success' => false, 'error' => 'Failed to add leistung')); +} diff --git a/ajax/add_product.php b/ajax/add_product.php new file mode 100644 index 0000000..45213f8 --- /dev/null +++ b/ajax/add_product.php @@ -0,0 +1,61 @@ + + * + * AJAX: Produkt hinzufügen + */ + +if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1'); +if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1'); +if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1'); +if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1'); + +// 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"); + +dol_include_once('/stundenzettel/class/stundenzettel.class.php'); + +header('Content-Type: application/json'); + +// Security check +if (!$user->hasRight('stundenzettel', 'write')) { + echo json_encode(array('success' => false, 'error' => 'Permission denied')); + exit; +} + +$stundenzettel_id = GETPOST('stundenzettel_id', 'int'); +$fk_product = GETPOST('fk_product', 'int'); +$qty = GETPOST('qty', 'int'); +$label = GETPOST('label', 'alpha'); +$description = GETPOST('description', 'restricthtml'); + +if (empty($stundenzettel_id)) { + echo json_encode(array('success' => false, 'error' => 'Missing stundenzettel_id')); + exit; +} + +$stundenzettel = new Stundenzettel($db); +if ($stundenzettel->fetch($stundenzettel_id) <= 0) { + echo json_encode(array('success' => false, 'error' => 'Stundenzettel not found')); + exit; +} + +// Nur im Entwurf bearbeitbar +if ($stundenzettel->status != Stundenzettel::STATUS_DRAFT) { + echo json_encode(array('success' => false, 'error' => 'Stundenzettel is not in draft status')); + exit; +} + +$result = $stundenzettel->addProduct($fk_product, null, null, $qty, 0, 'added', $description); + +if ($result > 0) { + echo json_encode(array( + 'success' => true, + 'product_line_id' => $result + )); +} else { + echo json_encode(array('success' => false, 'error' => 'Failed to add product')); +} diff --git a/ajax/update_qty.php b/ajax/update_qty.php new file mode 100644 index 0000000..908433a --- /dev/null +++ b/ajax/update_qty.php @@ -0,0 +1,59 @@ + + * + * AJAX: Produktmenge aktualisieren + */ + +if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1'); +if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1'); +if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1'); +if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1'); + +// 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"); + +dol_include_once('/stundenzettel/class/stundenzettel.class.php'); + +header('Content-Type: application/json'); + +// Security check +if (!$user->hasRight('stundenzettel', 'write')) { + echo json_encode(array('success' => false, 'error' => 'Permission denied')); + exit; +} + +$stundenzettel_id = GETPOST('stundenzettel_id', 'int'); +$line_id = GETPOST('line_id', 'int'); +$qty_done = GETPOST('qty_done', 'int'); + +if (empty($stundenzettel_id) || empty($line_id)) { + echo json_encode(array('success' => false, 'error' => 'Missing parameters')); + exit; +} + +$stundenzettel = new Stundenzettel($db); +if ($stundenzettel->fetch($stundenzettel_id) <= 0) { + echo json_encode(array('success' => false, 'error' => 'Stundenzettel not found')); + exit; +} + +// Nur im Entwurf bearbeitbar +if ($stundenzettel->status != Stundenzettel::STATUS_DRAFT) { + echo json_encode(array('success' => false, 'error' => 'Stundenzettel is not in draft status')); + exit; +} + +$result = $stundenzettel->updateProductQty($line_id, $qty_done); + +if ($result > 0) { + echo json_encode(array( + 'success' => true, + 'qty_done' => $qty_done + )); +} else { + echo json_encode(array('success' => false, 'error' => 'Failed to update qty')); +} diff --git a/card.php b/card.php new file mode 100644 index 0000000..7dc17f8 --- /dev/null +++ b/card.php @@ -0,0 +1,1766 @@ + + * + * 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.'/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 = ''; + 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 .= ''; + } + } + 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 +$permissiontoread = $user->hasRight('stundenzettel', 'read'); +$permissiontoadd = $user->hasRight('stundenzettel', 'write'); +$permissiontodelete = $user->hasRight('stundenzettel', 'delete'); +$permissiontovalidate = $user->hasRight('stundenzettel', 'validate'); + +/* + * 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'); + } 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'); + } else { + setEventMessages($object->error, $object->errors, 'errors'); + } +} + +// Delete +if ($action == 'confirm_delete' && $confirm == 'yes' && $permissiontodelete) { + $fk_commande = $object->fk_commande; // Speichern vor dem Löschen + $result = $object->delete($user); + if ($result > 0) { + setEventMessages($langs->trans('StundenzettelDeleted'), null, 'mesgs'); + // 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'); + + $result = $object->addLeistung($user, $date, $time_start, $time_end, $description); + 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'); + + $result = $object->updateLeistung($leistung_id, $date, $time_start, $time_end, $description); + 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 = GETPOST('qty_done', 'int'); + + $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 = GETPOST('add_product_qty', 'int'); + $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 das nicht verbaut wird) +if ($action == 'add_entfaellt' && $permissiontoadd) { + $entfaellt_product_raw = GETPOST('entfaellt_product', 'alpha'); + $qty = GETPOST('entfaellt_qty', 'int'); + $reason = GETPOST('entfaellt_description', 'restricthtml'); + + // Prüfen ob es ein Freitext-Produkt ist (Format: "freetext_ROWID") + $fk_product = 0; + $freetext_description = ''; + if (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 ($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_product = cd.fk_product 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) { + // Produkt zum Stundenzettel hinzufügen mit origin='omitted' + $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) + $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; +} + +// Mehraufwand hinzufügen (zusätzliches Produkt nicht aus Auftrag) +if ($action == 'add_mehraufwand' && $permissiontoadd) { + $fk_product = GETPOST('mehraufwand_product', 'int'); + $qty = GETPOST('mehraufwand_qty', 'int'); + $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 = GETPOST('qty_done', 'int'); + $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 = ".((int)$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'); + +// JavaScript für Mengenprüfung +print ''; + +// 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 '
'; + print ''; + print ''; + + print dol_get_fiche_head(array(), ''); + + print ''; + + // Auftrag + print ''; + + // Datum + print ''; + + // Notiz öffentlich + print ''; + + // Notiz privat + print ''; + + print '
'.$langs->trans("Order").''; + if ($fk_commande > 0) { + $order = new Commande($db); + $order->fetch($fk_commande); + print $order->getNomUrl(1); + print ''; + } 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 ''; + } + print '
'.$langs->trans("Date").''; + print $form->selectDate(dol_now(), 'date_stundenzettel', 0, 0, 0, '', 1, 1); + print '
'.$langs->trans("NotePublic").''; + $doleditor = new DolEditor('note_public', '', '', 150, 'dolibarr_notes', '', false, true, getDolGlobalString('FCKEDITOR_ENABLE_NOTE_PUBLIC'), ROWS_5, '90%'); + print $doleditor->Create(1); + print '
'.$langs->trans("NotePrivate").''; + $doleditor = new DolEditor('note_private', '', '', 150, 'dolibarr_notes', '', false, true, getDolGlobalString('FCKEDITOR_ENABLE_NOTE_PRIVATE'), ROWS_5, '90%'); + print $doleditor->Create(1); + print '
'; + + print dol_get_fiche_end(); + + print '
'; + print ''; + print '   '; + print ''.$langs->trans("Cancel").''; + print '
'; + + print '
'; +} + +// 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 = ''.$langs->trans("BackToList").''; + + dol_banner_tab($object, 'id', $linkback, 1, 'rowid', 'ref', '', '', 0, '', '', 1); + + print '
'; + print '
'; + + // ============================================= + // BEREICH: LEISTUNGEN (immer anzeigen außer bei Notizen) + // ============================================= + if ($subtab != 'notes') { + // 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 '

'.$langs->trans("Leistungen").'

'; + print '
'; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) { + print ''; // Edit + print ''; // Delete + } + print ''; + + 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 ''; + print ''; + print ''; + print ''; + print ''; + + print ''; + print ''; + print ''; + print ''; + print ''; + // Save Button + print ''; + // Cancel Button + print ''; + print ''; + print ''; + } else { + // Normale Anzeige + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) { + // Edit Button + print ''; + // Delete Button + print ''; + } + print ''; + } + } + } else { + $colspan = ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) ? 7 : 5; + print ''; + } + + // 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 ''; + print ''; + print ''; + print ''; + + // Datum ist fix (Stundenzettel gilt für einen Tag) + print ''; + print ''; + + // Beginn-Zeit + print ''; + + // Ende-Zeit + print ''; + + // Dauer-Spalte - leer lassen (wird nach Speichern berechnet) + print ''; + + // Beschreibung (Tagesbeschreibung - größeres Textarea, 5 Zeilen) + print ''; + + // Action (colspan=2 für beide Button-Spalten) + print ''; + + print ''; + print ''; + } + + // 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) ? 7 : 5; + print ''; + print ''; + print ''; + print ''; + print ''; + } + } + + print '
'.$langs->trans("Date").''.$langs->trans("LeistungTimeStart").''.$langs->trans("LeistungTimeEnd").''.$langs->trans("Duration").''.$langs->trans("Description").'
'.$form->selectDate($db->jdate($leistung->date_leistung), 'leistung_date', 0, 0, 0, '', 1, 0).''; + if ($leistung->duration > 0) { + $hours = floor($leistung->duration / 60); + $mins = $leistung->duration % 60; + print $hours.'h '.sprintf('%02d', $mins).'min'; + } + print ''; + print ''; + print ''; + print ''; + print '
'.dol_print_date($db->jdate($leistung->date_leistung), 'day').''.$leistung->time_start.''.$leistung->time_end.''; + if ($leistung->duration > 0) { + $hours = floor($leistung->duration / 60); + $mins = $leistung->duration % 60; + print $hours.'h '.sprintf('%02d', $mins).'min'; + } + print ''.dol_htmlentitiesbr($leistung->description).''; + print ''; + print ''; + print ''; + print '
'.$langs->trans("NoRecordFound").'
'.dol_print_date($object->date_stundenzettel, 'day').''; + if ($showTextInput) { + print ''; + } else { + print ''; + } + print ''; + if ($showTextInput) { + print ''; + } else { + print ''; + } + print ''; + print ''; + print ''; + print ''; + print ''; + print '
'.$langs->trans("Total").''.$hours.'h '.sprintf('%02d', $mins).'min
'; + print '
'; + } + + // ============================================= + // 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 '
'; + print ''; + $colspanProducts = ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) ? 8 : 4; + print ''; + print ''; + print ''; + print ''; + print ''; + if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) { + print ''; // Minus + print ''; // Plus + print ''; // Save + print ''; // Delete + } + print ''; + print ''; + + // 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 ''; + + // Produkt + print ''; + + // Menge aus Auftrag + print ''; + + // Menge erledigt + print ''; + + if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) { + // Minus Button + print ''; + + // Plus Button - mit Warnung bei Überschreitung der Auftragsmenge + // $totalQtyAllStz und $qtyOriginal wurden am Anfang der Schleife berechnet + $qtyOriginal = (float)$prod->qty_original; + print ''; + + // Save Button + print ''; + + // Delete Button + print ''; + } + + // Herkunft + print ''; + + print ''; + } + } else { + $colspan = ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) ? 8 : 4; + print ''; + } + + // Formular zum Hinzufügen von Produkten (nur im Entwurf) + if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) { + print ''; + print ''; + print ''; + + print ''; + print ''; + print ''; + print ''; + + // Produkt-Auswahl (colspan für Produkt + QtyFromOrder) + print ''; + + // Menge + print ''; + + // Hinzufügen-Button (colspan für Minus, Plus, Save, Delete) + print ''; + + // Origin - leer + print ''; + + print ''; + print ''; + } + + print '
'.$langs->trans("Products").' - '.$langs->trans("ProductsDesc").'
'.$langs->trans("Product").''.$langs->trans("QtyFromOrder").''.$langs->trans("QtyUsed").''.$langs->trans("Origin").'
'; + if ($prod->fk_product > 0) { + print ''; + print img_picto('', 'product', 'class="pictofixedwidth"'); + print $prod->product_ref.' - '.$prod->product_label; + print ''; + } 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 ''.$displayText.''; + } + // Mehraufwand-Warnung anzeigen wenn überschritten + if ($isExceeded) { + print ' +'.formatQty($exceededQty).' '.$langs->trans("Mehraufwand").''; + } + print ''.formatQty($prod->qty_original).''; + // 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 '
'; + print ''; + print ''; + print ''; + print ''; + print '
'; + } else { + print formatQty($prod->qty_done); + } + print '
'; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + if ($prod->origin == 'order') { + print ''.$langs->trans("FromOrder").''; + } else { + print ''.$langs->trans("Added").''; + } + print '
'.$langs->trans("NoRecordFound").'
'.$langs->trans("AddProduct").'
'; + print $form->select_produits('', 'add_product_id', '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth300', 0, '', null, 1); + print '
'.$langs->trans("OrFreeText").':
'; + print ''; + print '
'; + print ''; + print ''; + print ''; + print '
'; + print '
'; + + // ============================================= + // 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); + } + + print '
'; + print ''; + print ''; + + // Zuerst bereits erfasste Entfällt-Produkte anzeigen + $entfaelltProducts = array(); + foreach ($object->products as $prod) { + if ($prod->origin == 'omitted') { + $entfaelltProducts[] = $prod; + } + } + + if (count($entfaelltProducts) > 0) { + foreach ($entfaelltProducts as $prod) { + print ''; + + // Produkt + print ''; + + // Menge + print ''; + + // Grund/Beschreibung + print ''; + + // Save + print ''; + + // Delete + print ''; + + print ''; + } + } + + // 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_product = cd.fk_product 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 = 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 ''; + print ''; + print ''; + + print ''; + print ''; + print ''; + print ''; + + // Produkt-Auswahl - NUR Produkte aus dem Auftrag mit verfügbarer Menge + print ''; + + // Menge (mit dynamischem max basierend auf Produktauswahl) + print ''; + + // Grund + print ''; + + // Hinzufügen-Button (colspan für beide Button-Spalten) + print ''; + + print ''; + print ''; + + // JavaScript für dynamische Max-Menge + print ''; + + print '
'.$langs->trans("Entfaellt").' - '.$langs->trans("EntfaelltDesc").'
'; + if ($prod->fk_product > 0) { + print ''; + print img_picto('', 'product', 'class="pictofixedwidth"'); + print $prod->product_ref.' - '.$prod->product_label; + print ''; + } 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 ''.$displayText.''; + } + print ' '.$langs->trans("Entfaellt").''; + print ''; + print '
'; + print ''; + print ''; + print ''; + print ''; + print '
'; + print '
'; + if (!empty($prod->description)) { + print dol_htmlentitiesbr($prod->description); + } else { + print '-'; + } + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print '
'.$langs->trans("AddEntfaellt").'
'; + if ($hasAvailableProducts) { + print ''; + } else { + print ''.$langs->trans("AllProductsUsedOrOmitted").''; + } + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print '
'; + print '
'; + } + + // ============================================= + // 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); + } + + print '
'; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; // Save + print ''; // Delete + print ''; + + // Zuerst bereits erfasste Mehraufwand-Produkte anzeigen + $mehraufwandProducts = array(); + foreach ($object->products as $prod) { + if ($prod->origin == 'additional') { + $mehraufwandProducts[] = $prod; + } + } + + if (count($mehraufwandProducts) > 0) { + foreach ($mehraufwandProducts as $prod) { + $qty = (float)$prod->qty_done; + + print ''; + + // Produkt + print ''; + + // Menge (qty_done) - editierbar + print ''; + + // Grund (nur wenn Produkt gesetzt, sonst ist description der Produktname) + print ''; + + // Save + print ''; + + // Delete + print ''; + + print ''; + } + } + + // Formular zum Hinzufügen - immer am Ende (unter den hinzugefügten) + if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) { + print ''; + print ''; + print ''; + + print ''; + print ''; + print ''; + print ''; + + // Produkt-Auswahl + print ''; + + // Menge + print ''; + + // Grund (optional) + print ''; + + // Hinzufügen-Button (colspan für Save, Delete) + print ''; + + print ''; + print ''; + } + + print '
'.$langs->trans("Mehraufwand").' - '.$langs->trans("MehraufwandDesc").'
'.$langs->trans("Product").''.$langs->trans("Qty").''.$langs->trans("Reason").'
'; + if ($prod->fk_product > 0) { + print ''; + print img_picto('', 'product', 'class="pictofixedwidth"'); + print $prod->product_ref.' - '.$prod->product_label; + print ''; + } 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 ''.$displayText.''; + } + print ' '.$langs->trans("Mehraufwand").''; + print ''; + if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) { + print '
'; + print ''; + print ''; + print ''; + print ''; + } else { + print formatQty($qty); + } + print '
'; + 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 ''; + print ''; + } else { + if ($prod->fk_product > 0 && !empty($prod->description)) { + print dol_escape_htmltag($prod->description); + } else { + print '-'; + } + } + print ''; + if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) { + print ''; + print ''; + print ''; + } + print ''; + if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) { + print ''; + print ''; + print ''; + } + print '
'.$langs->trans("AddMehraufwand").'
'; + print $form->select_produits('', 'mehraufwand_product', '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth200', 0, '', null, 1); + print '
'.$langs->trans("OrFreeText").':
'; + print ''; + print '
'; + print ''; + print ''; + print 'trans("Optional").')">'; + print ''; + print ''; + print '
'; + print '
'; + } + + // ============================================= + // 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); + } + + print '
'; + print ''; + print ''; + + // Vorhandene Notizen anzeigen + if (count($object->notes) > 0) { + foreach ($object->notes as $note) { + $isChecked = ($note->checked == 1); + print ''; + + // Checkbox + print ''; + + // Notiz-Text + print ''; + print dol_htmlentitiesbr($note->note); + print ''; + + // Datum + print ''; + + // Löschen + print ''; + + print ''; + } + } else { + print ''; + } + + // Formular zum Hinzufügen (nur im Entwurf) + if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) { + print ''; + print ''; + print ''; + print ''; + + // Leere Checkbox-Spalte + print ''; + + // Notiz-Eingabe + print ''; + + // Leere Datum-Spalte + print ''; + + // Hinzufügen-Button + print ''; + + print ''; + print ''; + } + + print '
'.$langs->trans("NotesMemo").' - '.$langs->trans("NotesForNextVisit").'
'; + if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) { + $newChecked = $isChecked ? 0 : 1; + print ''; + if ($isChecked) { + print ''; + } else { + print ''; + } + print ''; + } else { + if ($isChecked) { + print ''; + } else { + print ''; + } + } + print ''; + print dol_print_date($db->jdate($note->datec), 'dayhour'); + print ''; + if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) { + print ''; + print ''; + print ''; + } + print '
'.$langs->trans("NoNotesYet").'
'; + print ''; + print ''; + print ''; + print '
'; + print '
'; + } + + // ============================================= + // TAB: NOTIZEN + // ============================================= + if ($subtab == 'notes') { + if ($action == 'edit' && $permissiontoadd) { + // Bearbeitungsmodus + print '
'; + print ''; + print ''; + print ''; + print ''; + print ''; + + print ''; + + // Notiz öffentlich (für nächsten Termin) + print ''; + + // Notiz privat + print ''; + + print '
'.$langs->trans("NotePublic").'
('.$langs->trans("NotesForNextVisit").')
'; + $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 '
'.$langs->trans("NotePrivate").''; + $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 '
'; + + print '
'; + print ''; + print '   '; + print ''.$langs->trans("Cancel").''; + print '
'; + + print '
'; + } else { + // Ansicht + print ''; + + // Notiz öffentlich (für nächsten Termin) + print ''; + + // Notiz privat + print ''; + + print '
'.$langs->trans("NotePublic").'
('.$langs->trans("NotesForNextVisit").')
'; + if (!empty($object->note_public)) { + print dol_htmlentitiesbr($object->note_public); + } else { + print '-'; + } + print '
'.$langs->trans("NotePrivate").''; + if (!empty($object->note_private)) { + print dol_htmlentitiesbr($object->note_private); + } else { + print '-'; + } + print '
'; + + // Bearbeiten-Button + if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) { + print ''; + } + } + } + + print '
'; // fichecenter + + print dol_get_fiche_end(); + + // Buttons (immer anzeigen) + print '
'; + + if ($object->status == Stundenzettel::STATUS_DRAFT) { + if ($permissiontovalidate) { + print ''.$langs->trans("Validate").''; + } + if ($permissiontodelete) { + print ''.$langs->trans("Delete").''; + } + } + + if ($object->status == Stundenzettel::STATUS_VALIDATED) { + if ($permissiontoadd) { + print ''.$langs->trans("SetToDraft").''; + } + } + + print '
'; +} + +llxFooter(); +$db->close(); diff --git a/class/stundenzettel.class.php b/class/stundenzettel.class.php new file mode 100644 index 0000000..d3ca933 --- /dev/null +++ b/class/stundenzettel.class.php @@ -0,0 +1,954 @@ + + * + * Stundenzettel - Business Object Klasse + */ + +require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php'; + +/** + * Class Stundenzettel + */ +class Stundenzettel extends CommonObject +{ + /** + * @var string ID to identify managed object + */ + public $element = 'stundenzettel'; + + /** + * @var string Name of table without prefix + */ + public $table_element = 'stundenzettel'; + + /** + * @var string Picto + */ + public $picto = 'clock'; + + // Status constants + const STATUS_DRAFT = 0; + const STATUS_VALIDATED = 1; + const STATUS_INVOICED = 2; + const STATUS_CANCELED = 9; + + /** + * @var string Ref + */ + public $ref; + + /** + * @var int Auftrag ID + */ + public $fk_commande; + + /** + * @var int Rechnung ID (nach Übertrag) + */ + public $fk_facture; + + /** + * @var int Kunde ID + */ + public $fk_soc; + + /** + * @var int Ersteller + */ + public $fk_user_author; + + /** + * @var int Freigebender User + */ + public $fk_user_valid; + + /** + * @var int|string Datum des Stundenzettels + */ + public $date_stundenzettel; + + /** + * @var int|string Erstelldatum + */ + public $datec; + + /** + * @var int|string Freigabedatum + */ + public $date_valid; + + /** + * @var int Status + */ + public $status; + + /** + * @var string Private Notizen + */ + public $note_private; + + /** + * @var string Öffentliche Notizen + */ + public $note_public; + + /** + * @var array Leistungen + */ + public $leistungen = array(); + + /** + * @var array Produkte + */ + public $products = array(); + + /** + * @var array Notizen (abhakbare Merkzettel) + */ + public $notes = array(); + + /** + * Constructor + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + $this->db = $db; + } + + /** + * Create object into database + * + * @param User $user User that creates + * @param bool $notrigger false=launch triggers after, true=disable triggers + * @return int <0 if KO, Id of created object if OK + */ + public function create($user, $notrigger = false) + { + global $conf; + + $error = 0; + + // Generate ref + if (empty($this->ref)) { + $this->ref = $this->getNextNumRef(); + } + + $this->db->begin(); + + $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." ("; + $sql .= "ref, entity, fk_commande, fk_soc, fk_user_author,"; + $sql .= "date_stundenzettel, datec, status, note_private, note_public"; + $sql .= ") VALUES ("; + $sql .= "'".$this->db->escape($this->ref)."',"; + $sql .= ((int)$conf->entity).","; + $sql .= ((int)$this->fk_commande).","; + $sql .= ((int)$this->fk_soc).","; + $sql .= ((int)$user->id).","; + $sql .= "'".$this->db->idate($this->date_stundenzettel)."',"; + $sql .= "'".$this->db->idate(dol_now())."',"; + $sql .= "0,"; // STATUS_DRAFT + $sql .= ($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL").","; + $sql .= ($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL"); + $sql .= ")"; + + dol_syslog(get_class($this)."::create", LOG_DEBUG); + $resql = $this->db->query($sql); + if (!$resql) { + $error++; + $this->errors[] = "Error ".$this->db->lasterror(); + } + + if (!$error) { + $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element); + } + + if (!$error) { + $this->db->commit(); + return $this->id; + } else { + $this->db->rollback(); + return -1; + } + } + + /** + * Load object in memory from the database + * + * @param int $id Id object + * @param string $ref Ref + * @return int <0 if KO, 0 if not found, >0 if OK + */ + public function fetch($id, $ref = null) + { + $sql = "SELECT s.rowid, s.ref, s.entity, s.fk_commande, s.fk_facture, s.fk_soc,"; + $sql .= " s.fk_user_author, s.fk_user_valid, s.date_stundenzettel, s.datec,"; + $sql .= " s.date_valid, s.status, s.note_private, s.note_public, s.tms"; + $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as s"; + if ($ref) { + $sql .= " WHERE s.ref = '".$this->db->escape($ref)."'"; + } else { + $sql .= " WHERE s.rowid = ".((int)$id); + } + + dol_syslog(get_class($this)."::fetch", LOG_DEBUG); + $resql = $this->db->query($sql); + if ($resql) { + if ($this->db->num_rows($resql)) { + $obj = $this->db->fetch_object($resql); + + $this->id = $obj->rowid; + $this->ref = $obj->ref; + $this->entity = $obj->entity; + $this->fk_commande = $obj->fk_commande; + $this->fk_facture = $obj->fk_facture; + $this->fk_soc = $obj->fk_soc; + $this->fk_user_author = $obj->fk_user_author; + $this->fk_user_valid = $obj->fk_user_valid; + $this->date_stundenzettel = $this->db->jdate($obj->date_stundenzettel); + $this->datec = $this->db->jdate($obj->datec); + $this->date_valid = $this->db->jdate($obj->date_valid); + $this->status = $obj->status; + $this->note_private = $obj->note_private; + $this->note_public = $obj->note_public; + + // Load lines + $this->fetchLeistungen(); + $this->fetchProducts(); + $this->fetchNotes(); + + return 1; + } else { + return 0; + } + } else { + $this->error = "Error ".$this->db->lasterror(); + return -1; + } + } + + /** + * Update object into database + * + * @param User $user User that modifies + * @param bool $notrigger false=launch triggers after, true=disable triggers + * @return int <0 if KO, >0 if OK + */ + public function update($user, $notrigger = false) + { + $error = 0; + + $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET"; + $sql .= " date_stundenzettel = '".$this->db->idate($this->date_stundenzettel)."',"; + $sql .= " note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL").","; + $sql .= " note_public = ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL"); + $sql .= " WHERE rowid = ".((int)$this->id); + + dol_syslog(get_class($this)."::update", LOG_DEBUG); + $resql = $this->db->query($sql); + if (!$resql) { + $error++; + $this->errors[] = "Error ".$this->db->lasterror(); + } + + if (!$error) { + return 1; + } else { + return -1; + } + } + + /** + * Validate object + * + * @param User $user User that validates + * @return int <0 if KO, >0 if OK + */ + public function validate($user) + { + $error = 0; + + $this->db->begin(); + + $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET"; + $sql .= " status = ".self::STATUS_VALIDATED.","; + $sql .= " fk_user_valid = ".((int)$user->id).","; + $sql .= " date_valid = '".$this->db->idate(dol_now())."'"; + $sql .= " WHERE rowid = ".((int)$this->id); + + dol_syslog(get_class($this)."::validate", LOG_DEBUG); + $resql = $this->db->query($sql); + if (!$resql) { + $error++; + $this->errors[] = "Error ".$this->db->lasterror(); + } + + if (!$error) { + $this->status = self::STATUS_VALIDATED; + $this->fk_user_valid = $user->id; + $this->date_valid = dol_now(); + + // Update tracking table + $this->updateTracking(); + + $this->db->commit(); + return 1; + } else { + $this->db->rollback(); + return -1; + } + } + + /** + * Set to draft + * + * @param User $user User that reopens + * @return int <0 if KO, >0 if OK + */ + public function setDraft($user) + { + $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET"; + $sql .= " status = ".self::STATUS_DRAFT.","; + $sql .= " fk_user_valid = NULL,"; + $sql .= " date_valid = NULL"; + $sql .= " WHERE rowid = ".((int)$this->id); + + dol_syslog(get_class($this)."::setDraft", LOG_DEBUG); + $resql = $this->db->query($sql); + if ($resql) { + $this->status = self::STATUS_DRAFT; + return 1; + } else { + $this->error = "Error ".$this->db->lasterror(); + return -1; + } + } + + /** + * Delete object in database + * + * @param User $user User that deletes + * @param bool $notrigger false=launch triggers after, true=disable triggers + * @return int <0 if KO, >0 if OK + */ + public function delete($user, $notrigger = false) + { + $error = 0; + + $this->db->begin(); + + // Delete notes + $sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_note WHERE fk_stundenzettel = ".((int)$this->id); + $resql = $this->db->query($sql); + if (!$resql) $error++; + + // Delete leistungen + $sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_leistung WHERE fk_stundenzettel = ".((int)$this->id); + $resql = $this->db->query($sql); + if (!$resql) $error++; + + // Delete products + $sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE fk_stundenzettel = ".((int)$this->id); + $resql = $this->db->query($sql); + if (!$resql) $error++; + + // Delete main record + if (!$error) { + $sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int)$this->id); + $resql = $this->db->query($sql); + if (!$resql) $error++; + } + + if (!$error) { + $this->db->commit(); + return 1; + } else { + $this->db->rollback(); + return -1; + } + } + + /** + * Load leistungen + * + * @return int <0 if KO, number of lines if OK + */ + public function fetchLeistungen() + { + $this->leistungen = array(); + + $sql = "SELECT rowid, fk_user, date_leistung, time_start, time_end, duration, description, rang"; + $sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel_leistung"; + $sql .= " WHERE fk_stundenzettel = ".((int)$this->id); + $sql .= " ORDER BY rang, date_leistung, time_start"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $this->leistungen[] = $obj; + } + return count($this->leistungen); + } + return -1; + } + + /** + * Load products + * + * @return int <0 if KO, number of lines if OK + */ + public function fetchProducts() + { + $this->products = array(); + + // Produkte laden, bei Freitext-Produkten Beschreibung aus commandedet holen falls leer + $sql = "SELECT sp.rowid, sp.fk_product, sp.fk_commandedet, sp.fk_manager_line,"; + $sql .= " sp.product_ref, sp.product_label,"; + $sql .= " CASE WHEN sp.description IS NULL OR sp.description = '' THEN cd.description ELSE sp.description END as description,"; + $sql .= " sp.qty_original, sp.qty_done, sp.qty_cumulated, sp.origin, sp.rang"; + $sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product as sp"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."commandedet as cd ON cd.rowid = sp.fk_commandedet"; + $sql .= " WHERE sp.fk_stundenzettel = ".((int)$this->id); + $sql .= " ORDER BY sp.rang"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $this->products[] = $obj; + } + return count($this->products); + } + return -1; + } + + /** + * Add a leistung + * + * @param User $user User + * @param string $date Date + * @param string $time_start Start time + * @param string $time_end End time + * @param string $description Description + * @return int <0 if KO, >0 if OK + */ + public function addLeistung($user, $date, $time_start = null, $time_end = null, $description = '') + { + global $langs; + + // Calculate duration + $duration = 0; + if ($time_start && $time_end) { + $start = strtotime($time_start); + $end = strtotime($time_end); + $duration = ($end - $start) / 60; // in minutes + } + + // Überlappungsprüfung: Prüfen ob für diese Zeit bereits eine Leistung existiert + if ($time_start && $time_end) { + $sqlCheck = "SELECT rowid, time_start, time_end FROM ".MAIN_DB_PREFIX."stundenzettel_leistung"; + $sqlCheck .= " WHERE fk_stundenzettel = ".((int)$this->id); + $sqlCheck .= " AND time_start IS NOT NULL AND time_end IS NOT NULL"; + $resqlCheck = $this->db->query($sqlCheck); + if ($resqlCheck) { + $newStart = strtotime($time_start); + $newEnd = strtotime($time_end); + while ($objCheck = $this->db->fetch_object($resqlCheck)) { + $existStart = strtotime($objCheck->time_start); + $existEnd = strtotime($objCheck->time_end); + // Überlappung: Start1 < End2 UND Start2 < End1 + if ($newStart < $existEnd && $existStart < $newEnd) { + $this->error = $langs->trans("ErrorTimeOverlap", $objCheck->time_start, $objCheck->time_end); + return -2; + } + } + } + } + + // Get next rang + $sql = "SELECT MAX(rang) as maxrang FROM ".MAIN_DB_PREFIX."stundenzettel_leistung WHERE fk_stundenzettel = ".((int)$this->id); + $resql = $this->db->query($sql); + $rang = 0; + if ($resql) { + $obj = $this->db->fetch_object($resql); + $rang = $obj->maxrang + 1; + } + + $sql = "INSERT INTO ".MAIN_DB_PREFIX."stundenzettel_leistung ("; + $sql .= "fk_stundenzettel, fk_user, date_leistung, time_start, time_end, duration, description, rang"; + $sql .= ") VALUES ("; + $sql .= ((int)$this->id).","; + $sql .= ((int)$user->id).","; + $sql .= "'".$this->db->idate($date)."',"; + $sql .= ($time_start ? "'".$this->db->escape($time_start)."'" : "NULL").","; + $sql .= ($time_end ? "'".$this->db->escape($time_end)."'" : "NULL").","; + $sql .= ((int)$duration).","; + $sql .= "'".$this->db->escape($description)."',"; + $sql .= ((int)$rang); + $sql .= ")"; + + $resql = $this->db->query($sql); + if ($resql) { + return $this->db->last_insert_id(MAIN_DB_PREFIX."stundenzettel_leistung"); + } + return -1; + } + + /** + * Update a leistung + * + * @param int $leistung_id Leistung ID + * @param string $date Date + * @param string $time_start Start time + * @param string $time_end End time + * @param string $description Description + * @return int <0 if KO, >0 if OK + */ + public function updateLeistung($leistung_id, $date, $time_start = null, $time_end = null, $description = '') + { + global $langs; + + // Calculate duration + $duration = 0; + if ($time_start && $time_end) { + $start = strtotime($time_start); + $end = strtotime($time_end); + $duration = ($end - $start) / 60; // in minutes + } + + // Überlappungsprüfung: Prüfen ob für diese Zeit bereits eine andere Leistung existiert + if ($time_start && $time_end) { + $sqlCheck = "SELECT rowid, time_start, time_end FROM ".MAIN_DB_PREFIX."stundenzettel_leistung"; + $sqlCheck .= " WHERE fk_stundenzettel = ".((int)$this->id); + $sqlCheck .= " AND rowid != ".((int)$leistung_id); // Aktuelle Leistung ausschließen + $sqlCheck .= " AND time_start IS NOT NULL AND time_end IS NOT NULL"; + $resqlCheck = $this->db->query($sqlCheck); + if ($resqlCheck) { + $newStart = strtotime($time_start); + $newEnd = strtotime($time_end); + while ($objCheck = $this->db->fetch_object($resqlCheck)) { + $existStart = strtotime($objCheck->time_start); + $existEnd = strtotime($objCheck->time_end); + // Überlappung: Start1 < End2 UND Start2 < End1 + if ($newStart < $existEnd && $existStart < $newEnd) { + $this->error = $langs->trans("ErrorTimeOverlap", $objCheck->time_start, $objCheck->time_end); + return -2; + } + } + } + } + + $sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_leistung SET"; + $sql .= " date_leistung = '".$this->db->idate($date)."',"; + $sql .= " time_start = ".($time_start ? "'".$this->db->escape($time_start)."'" : "NULL").","; + $sql .= " time_end = ".($time_end ? "'".$this->db->escape($time_end)."'" : "NULL").","; + $sql .= " duration = ".((int)$duration).","; + $sql .= " description = '".$this->db->escape($description)."'"; + $sql .= " WHERE rowid = ".((int)$leistung_id); + $sql .= " AND fk_stundenzettel = ".((int)$this->id); + + $resql = $this->db->query($sql); + if ($resql) { + return 1; + } + $this->error = $this->db->lasterror(); + return -1; + } + + /** + * Delete a leistung + * + * @param int $leistung_id Leistung ID + * @return int <0 if KO, >0 if OK + */ + public function deleteLeistung($leistung_id) + { + $sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_leistung"; + $sql .= " WHERE rowid = ".((int)$leistung_id); + $sql .= " AND fk_stundenzettel = ".((int)$this->id); + + $resql = $this->db->query($sql); + if ($resql) { + return 1; + } + $this->error = $this->db->lasterror(); + return -1; + } + + /** + * Add a product + * + * @param int $fk_product Product ID + * @param int $fk_commandedet Commandedet ID + * @param int $fk_manager_line Manager line ID + * @param float $qty_original Original qty + * @param float $qty_done Done qty + * @param string $origin Origin (order or added) + * @param string $description Description (for free-text products) + * @return int <0 if KO, >0 if OK + */ + public function addProduct($fk_product, $fk_commandedet = null, $fk_manager_line = null, $qty_original = 0, $qty_done = 0, $origin = 'order', $description = '') + { + global $db; + + // Get product info + $product_ref = ''; + $product_label = ''; + if ($fk_product > 0) { + require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; + $prod = new Product($this->db); + if ($prod->fetch($fk_product) > 0) { + $product_ref = $prod->ref; + $product_label = $prod->label; + } + } + + // Get next rang + $sql = "SELECT MAX(rang) as maxrang FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE fk_stundenzettel = ".((int)$this->id); + $resql = $this->db->query($sql); + $rang = 0; + if ($resql) { + $obj = $this->db->fetch_object($resql); + $rang = $obj->maxrang + 1; + } + + $sql = "INSERT INTO ".MAIN_DB_PREFIX."stundenzettel_product ("; + $sql .= "fk_stundenzettel, fk_product, fk_commandedet, fk_manager_line,"; + $sql .= "product_ref, product_label, description, qty_original, qty_done, origin, rang"; + $sql .= ") VALUES ("; + $sql .= ((int)$this->id).","; + $sql .= ($fk_product > 0 ? (int)$fk_product : "NULL").","; + $sql .= ($fk_commandedet > 0 ? (int)$fk_commandedet : "NULL").","; + $sql .= ($fk_manager_line > 0 ? (int)$fk_manager_line : "NULL").","; + $sql .= "'".$this->db->escape($product_ref)."',"; + $sql .= "'".$this->db->escape($product_label)."',"; + $sql .= "'".$this->db->escape($description)."',"; + $sql .= ((float)$qty_original).","; + $sql .= ((float)$qty_done).","; + $sql .= "'".$this->db->escape($origin)."',"; + $sql .= ((int)$rang); + $sql .= ")"; + + $resql = $this->db->query($sql); + if ($resql) { + return $this->db->last_insert_id(MAIN_DB_PREFIX."stundenzettel_product"); + } + return -1; + } + + /** + * Update product qty_done + * + * @param int $line_id Line ID + * @param float $qty_done Done qty + * @return int <0 if KO, >0 if OK + */ + public function updateProductQty($line_id, $qty_done) + { + $sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_product SET"; + $sql .= " qty_done = ".((float)$qty_done); + $sql .= " WHERE rowid = ".((int)$line_id); + $sql .= " AND fk_stundenzettel = ".((int)$this->id); + + $resql = $this->db->query($sql); + return $resql ? 1 : -1; + } + + /** + * Delete product from stundenzettel + * + * @param int $line_id Line ID + * @return int <0 if KO, >0 if OK + */ + public function deleteProduct($line_id) + { + $sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_product"; + $sql .= " WHERE rowid = ".((int)$line_id); + $sql .= " AND fk_stundenzettel = ".((int)$this->id); + + $resql = $this->db->query($sql); + return $resql ? 1 : -1; + } + + /** + * Update tracking table + * + * @return int <0 if KO, >0 if OK + */ + public function updateTracking() + { + // Sum up all products for this order across all stundenzettels + foreach ($this->products as $prod) { + if (!$prod->fk_commandedet) continue; + + // Get total done qty for this commandedet + $sql = "SELECT SUM(qty_done) as total_done FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; + $sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; + $sql .= " WHERE sp.fk_commandedet = ".((int)$prod->fk_commandedet); + $sql .= " AND s.status >= ".self::STATUS_VALIDATED; + + $resql = $this->db->query($sql); + $total_done = 0; + if ($resql && ($obj = $this->db->fetch_object($resql))) { + $total_done = $obj->total_done; + } + + // Update or insert tracking + $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel_tracking"; + $sql .= " WHERE fk_commande = ".((int)$this->fk_commande); + $sql .= " AND fk_commandedet = ".((int)$prod->fk_commandedet); + + $resql = $this->db->query($sql); + if ($resql && $this->db->num_rows($resql) > 0) { + // Update + $obj = $this->db->fetch_object($resql); + $remaining = $prod->qty_original - $total_done; + $status = 'open'; + if ($total_done >= $prod->qty_original) { + $status = 'done'; + } elseif ($total_done > 0) { + $status = 'partial'; + } + + $sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_tracking SET"; + $sql .= " qty_delivered = ".((float)$total_done).","; + $sql .= " qty_remaining = ".((float)$remaining).","; + $sql .= " status = '".$this->db->escape($status)."'"; + $sql .= " WHERE rowid = ".((int)$obj->rowid); + $this->db->query($sql); + } else { + // Insert + $sql = "INSERT INTO ".MAIN_DB_PREFIX."stundenzettel_tracking ("; + $sql .= "fk_commande, fk_product, fk_commandedet, fk_manager_line,"; + $sql .= "product_ref, product_label, qty_ordered, qty_delivered, qty_remaining, status"; + $sql .= ") VALUES ("; + $sql .= ((int)$this->fk_commande).","; + $sql .= ($prod->fk_product > 0 ? (int)$prod->fk_product : "NULL").","; + $sql .= ((int)$prod->fk_commandedet).","; + $sql .= ($prod->fk_manager_line > 0 ? (int)$prod->fk_manager_line : "NULL").","; + $sql .= "'".$this->db->escape($prod->product_ref)."',"; + $sql .= "'".$this->db->escape($prod->product_label)."',"; + $sql .= ((float)$prod->qty_original).","; + $sql .= ((float)$total_done).","; + $sql .= ((float)($prod->qty_original - $total_done)).","; + $sql .= "'open'"; + $sql .= ")"; + $this->db->query($sql); + } + } + + return 1; + } + + /** + * Get next reference number + * + * @return string Next ref + */ + public function getNextNumRef() + { + global $conf; + + $prefix = 'SZ'; + $year = date('Y'); + + $sql = "SELECT MAX(CAST(SUBSTRING(ref, ".(strlen($prefix.$year.'-') + 1).") AS UNSIGNED)) as maxnum"; + $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element; + $sql .= " WHERE ref LIKE '".$this->db->escape($prefix.$year)."-%'"; + $sql .= " AND entity = ".((int)$conf->entity); + + $resql = $this->db->query($sql); + if ($resql) { + $obj = $this->db->fetch_object($resql); + $num = $obj->maxnum + 1; + } else { + $num = 1; + } + + return $prefix.$year.'-'.sprintf('%05d', $num); + } + + /** + * Get status label + * + * @param int $mode 0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto + * @return string Label + */ + public function getLibStatut($mode = 0) + { + return $this->LibStatut($this->status, $mode); + } + + /** + * Return label of status + * + * @param int $status Status + * @param int $mode Mode + * @return string Label + */ + public function LibStatut($status, $mode = 0) + { + global $langs; + $langs->load("stundenzettel@stundenzettel"); + + if ($status == self::STATUS_DRAFT) { + $statusType = 'status0'; + $label = $langs->trans("StatusDraft"); + } elseif ($status == self::STATUS_VALIDATED) { + $statusType = 'status4'; + $label = $langs->trans("StatusValidated"); + } elseif ($status == self::STATUS_INVOICED) { + $statusType = 'status6'; + $label = $langs->trans("StatusInvoiced"); + } elseif ($status == self::STATUS_CANCELED) { + $statusType = 'status9'; + $label = $langs->trans("StatusCanceled"); + } + + return dolGetStatus($label, '', '', $statusType, $mode); + } + + /** + * Return URL link + * + * @param int $withpicto Picto + * @param string $option Option + * @return string Link + */ + public function getNomUrl($withpicto = 0, $option = '') + { + $result = ''; + $url = dol_buildpath('/stundenzettel/card.php?id='.$this->id, 1); + + $label = ''.$this->ref.''; + + if ($withpicto) { + $result .= img_object('', $this->picto, 'class="pictofixedwidth"'); + } + $result .= ''.$this->ref.''; + + return $result; + } + + /** + * Load notes (checkable memos) + * + * @return int <0 if KO, number of notes if OK + */ + public function fetchNotes() + { + $this->notes = array(); + + $sql = "SELECT rowid, fk_user, note, checked, rang, datec"; + $sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel_note"; + $sql .= " WHERE fk_stundenzettel = ".((int)$this->id); + $sql .= " ORDER BY rang, datec"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $this->notes[] = $obj; + } + return count($this->notes); + } + return -1; + } + + /** + * Add a note + * + * @param User $user User + * @param string $note Note text + * @return int <0 if KO, >0 if OK (rowid) + */ + public function addNote($user, $note) + { + if (empty($note)) { + return -1; + } + + // Get next rang + $sql = "SELECT MAX(rang) as maxrang FROM ".MAIN_DB_PREFIX."stundenzettel_note WHERE fk_stundenzettel = ".((int)$this->id); + $resql = $this->db->query($sql); + $rang = 0; + if ($resql) { + $obj = $this->db->fetch_object($resql); + $rang = $obj->maxrang + 1; + } + + $sql = "INSERT INTO ".MAIN_DB_PREFIX."stundenzettel_note ("; + $sql .= "fk_stundenzettel, fk_user, note, checked, rang, datec"; + $sql .= ") VALUES ("; + $sql .= ((int)$this->id).","; + $sql .= ((int)$user->id).","; + $sql .= "'".$this->db->escape($note)."',"; + $sql .= "0,"; // not checked + $sql .= ((int)$rang).","; + $sql .= "'".$this->db->idate(dol_now())."'"; + $sql .= ")"; + + $resql = $this->db->query($sql); + if ($resql) { + return $this->db->last_insert_id(MAIN_DB_PREFIX."stundenzettel_note"); + } + return -1; + } + + /** + * Update note checked status + * + * @param int $note_id Note ID + * @param int $checked 0=unchecked, 1=checked + * @return int <0 if KO, >0 if OK + */ + public function updateNoteStatus($note_id, $checked) + { + $sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_note SET"; + $sql .= " checked = ".((int)$checked); + $sql .= " WHERE rowid = ".((int)$note_id); + $sql .= " AND fk_stundenzettel = ".((int)$this->id); + + $resql = $this->db->query($sql); + return $resql ? 1 : -1; + } + + /** + * Delete a note + * + * @param int $note_id Note ID + * @return int <0 if KO, >0 if OK + */ + public function deleteNote($note_id) + { + $sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_note"; + $sql .= " WHERE rowid = ".((int)$note_id); + $sql .= " AND fk_stundenzettel = ".((int)$this->id); + + $resql = $this->db->query($sql); + return $resql ? 1 : -1; + } + + /** + * Get unchecked notes (for display on order page) + * + * @return array Array of unchecked notes + */ + public function getUncheckedNotes() + { + $notes = array(); + + $sql = "SELECT rowid, fk_user, note, rang, datec"; + $sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel_note"; + $sql .= " WHERE fk_stundenzettel = ".((int)$this->id); + $sql .= " AND checked = 0"; + $sql .= " ORDER BY rang, datec"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $notes[] = $obj; + } + } + return $notes; + } +} diff --git a/core/modules/modStundenzettel.class.php b/core/modules/modStundenzettel.class.php new file mode 100644 index 0000000..1fae773 --- /dev/null +++ b/core/modules/modStundenzettel.class.php @@ -0,0 +1,469 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +/** + * \defgroup stundenzettel Module Stundenzettel + * \brief Stundenzettel-Verwaltung für Aufträge + */ + +/** + * \file core/modules/modStundenzettel.class.php + * \ingroup stundenzettel + * \brief Modulbeschreibung und Setup + */ + +include_once DOL_DOCUMENT_ROOT.'/core/modules/DolibarrModules.class.php'; + +/** + * Class modStundenzettel + */ +class modStundenzettel extends DolibarrModules +{ + /** + * Constructor + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + global $langs, $conf; + + $this->db = $db; + + // Modul-ID (muss eindeutig sein) + $this->numero = 500200; + + // Familie/Kategorie + $this->family = "crm"; + + // Position im Menü (zwischen Einkauf=40 und Rechnung=50) + $this->module_position = '45'; + + // Modulname + $this->name = preg_replace('/^mod/i', '', get_class($this)); + + // Beschreibung + $this->description = "Stundenzettel-Verwaltung für Aufträge - Dokumentation von Arbeitszeiten und Materialverbrauch"; + $this->descriptionlong = "Verwaltet Stundenzettel für Kundenaufträge. Ermöglicht die Dokumentation von Arbeitszeiten, verbrauchten Materialien und Notizen. Integration mit SubtotalTitle für Produktgruppen-Unterstützung."; + + // Version + $this->version = '1.1.0'; + + // Autor + $this->editor_name = 'Data IT Solution'; + $this->editor_url = 'https://data-it-solution.de'; + + // Konstanten + $this->const_name = 'MAIN_MODULE_'.strtoupper($this->name); + + // Pfade + $this->special = 0; + $this->picto = 'clock'; + + // Abhängigkeiten + $this->depends = array('modCommande'); // Aufträge erforderlich + $this->requiredby = array(); + $this->conflictwith = array(); + + // PHP-Version + $this->phpmin = array(7, 4); + + // Dolibarr-Version + $this->need_dolibarr_version = array(16, 0); + + // Daten-Verzeichnisse + $this->dirs = array('/stundenzettel/temp'); + + // Konfiguration + $this->config_page_url = array("setup.php@stundenzettel"); + + // Konstanten + $this->const = array( + 0 => array( + 'STUNDENZETTEL_ADDON', + 'chaine', + 'mod_stundenzettel_standard', + 'Nummernkreis für Stundenzettel', + 0, + 'current', + 1 + ), + 1 => array( + 'STUNDENZETTEL_TIME_INPUT_MODE', + 'chaine', + 'dropdown', + 'Zeiteingabe-Modus: dropdown (15-Min-Takt) oder text (freie Eingabe)', + 0, + 'current', + 1 + ), + ); + + // Tabs - Tab im Auftrag (order = commande) + $this->tabs = array( + 'order:+stundenzettel:Stundenzettel:stundenzettel@stundenzettel:$user->hasRight("stundenzettel","read"):/custom/stundenzettel/stundenzettel_commande.php?id=__ID__' + ); + + // Boxen/Widgets + $this->boxes = array( + 0 => array( + 'file' => 'box_stundenzettel_recent@stundenzettel', + 'note' => 'Zuletzt bearbeitete Stundenzettel', + 'enabledbydefaulton' => 'Home' + ), + 1 => array( + 'file' => 'box_stundenzettel_open@stundenzettel', + 'note' => 'Offene Stundenzettel', + 'enabledbydefaulton' => 'Home' + ), + ); + + // Cronjobs + $this->cronjobs = array(); + + // Berechtigungen + $this->rights = array(); + $this->rights_class = 'stundenzettel'; + + $r = 0; + + // Lesen + $this->rights[$r][0] = $this->numero + $r; + $this->rights[$r][1] = 'Stundenzettel lesen'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'read'; + $this->rights[$r][5] = ''; + $r++; + + // Erstellen + $this->rights[$r][0] = $this->numero + $r; + $this->rights[$r][1] = 'Stundenzettel erstellen'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'write'; + $this->rights[$r][5] = ''; + $r++; + + // Freigeben + $this->rights[$r][0] = $this->numero + $r; + $this->rights[$r][1] = 'Stundenzettel freigeben'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'validate'; + $this->rights[$r][5] = ''; + $r++; + + // Löschen + $this->rights[$r][0] = $this->numero + $r; + $this->rights[$r][1] = 'Stundenzettel löschen'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'delete'; + $this->rights[$r][5] = ''; + $r++; + + // Hauptmenü + $this->menu = array(); + $r = 0; + + // Top-Menü + $this->menu[$r] = array( + 'fk_menu' => '', + 'type' => 'top', + 'titre' => 'Stundenzettel', + 'prefix' => img_picto('', 'clock', 'class="pictofixedwidth"'), + 'mainmenu' => 'stundenzettel', + 'leftmenu' => '', + 'url' => '/stundenzettel/index.php', + 'langs' => 'stundenzettel@stundenzettel', + 'position' => 45, + 'enabled' => '$conf->stundenzettel->enabled', + 'perms' => '$user->hasRight("stundenzettel", "read")', + 'target' => '', + 'user' => 0 + ); + $r++; + + // Linkes Menü - Übersicht + $this->menu[$r] = array( + 'fk_menu' => 'fk_mainmenu=stundenzettel', + 'type' => 'left', + 'titre' => 'Übersicht', + 'prefix' => img_picto('', 'home', 'class="pictofixedwidth"'), + 'mainmenu' => 'stundenzettel', + 'leftmenu' => 'stundenzettel_index', + 'url' => '/stundenzettel/index.php', + 'langs' => 'stundenzettel@stundenzettel', + 'position' => 100, + 'enabled' => '$conf->stundenzettel->enabled', + 'perms' => '$user->hasRight("stundenzettel", "read")', + 'target' => '', + 'user' => 0 + ); + $r++; + + // Linkes Menü - Liste + $this->menu[$r] = array( + 'fk_menu' => 'fk_mainmenu=stundenzettel', + 'type' => 'left', + 'titre' => 'Alle Stundenzettel', + 'prefix' => img_picto('', 'list', 'class="pictofixedwidth"'), + 'mainmenu' => 'stundenzettel', + 'leftmenu' => 'stundenzettel_list', + 'url' => '/stundenzettel/list.php', + 'langs' => 'stundenzettel@stundenzettel', + 'position' => 110, + 'enabled' => '$conf->stundenzettel->enabled', + 'perms' => '$user->hasRight("stundenzettel", "read")', + 'target' => '', + 'user' => 0 + ); + $r++; + + // Linkes Menü - Neuer Stundenzettel + $this->menu[$r] = array( + 'fk_menu' => 'fk_mainmenu=stundenzettel', + 'type' => 'left', + 'titre' => 'Neuer Stundenzettel', + 'prefix' => img_picto('', 'add', 'class="pictofixedwidth"'), + 'mainmenu' => 'stundenzettel', + 'leftmenu' => 'stundenzettel_new', + 'url' => '/stundenzettel/card.php?action=create', + 'langs' => 'stundenzettel@stundenzettel', + 'position' => 120, + 'enabled' => '$conf->stundenzettel->enabled', + 'perms' => '$user->hasRight("stundenzettel", "write")', + 'target' => '', + 'user' => 0 + ); + $r++; + + // Linkes Menü - Offene Stundenzettel (Entwürfe) + $this->menu[$r] = array( + 'fk_menu' => 'fk_mainmenu=stundenzettel', + 'type' => 'left', + 'titre' => 'Offene Stundenzettel', + 'prefix' => img_picto('', 'statut0', 'class="pictofixedwidth"'), + 'mainmenu' => 'stundenzettel', + 'leftmenu' => 'stundenzettel_open', + 'url' => '/stundenzettel/list.php?search_status=0', + 'langs' => 'stundenzettel@stundenzettel', + 'position' => 130, + 'enabled' => '$conf->stundenzettel->enabled', + 'perms' => '$user->hasRight("stundenzettel", "read")', + 'target' => '', + 'user' => 0 + ); + $r++; + + // Linkes Menü - Meine Stundenzettel + $this->menu[$r] = array( + 'fk_menu' => 'fk_mainmenu=stundenzettel', + 'type' => 'left', + 'titre' => 'Meine Stundenzettel', + 'prefix' => img_picto('', 'user', 'class="pictofixedwidth"'), + 'mainmenu' => 'stundenzettel', + 'leftmenu' => 'stundenzettel_my', + 'url' => '/stundenzettel/list.php?search_author=__USER_ID__', + 'langs' => 'stundenzettel@stundenzettel', + 'position' => 140, + 'enabled' => '$conf->stundenzettel->enabled', + 'perms' => '$user->hasRight("stundenzettel", "read")', + 'target' => '', + 'user' => 0 + ); + $r++; + + // Linkes Menü - Freigegebene Stundenzettel + $this->menu[$r] = array( + 'fk_menu' => 'fk_mainmenu=stundenzettel', + 'type' => 'left', + 'titre' => 'Freigegeben', + 'prefix' => img_picto('', 'statut4', 'class="pictofixedwidth"'), + 'mainmenu' => 'stundenzettel', + 'leftmenu' => 'stundenzettel_validated', + 'url' => '/stundenzettel/list.php?search_status=1', + 'langs' => 'stundenzettel@stundenzettel', + 'position' => 150, + 'enabled' => '$conf->stundenzettel->enabled', + 'perms' => '$user->hasRight("stundenzettel", "read")', + 'target' => '', + 'user' => 0 + ); + $r++; + } + + /** + * Funktion beim Aktivieren des Moduls + * + * @param string $options Options when enabling module + * @return int 1 if OK, 0 if KO + */ + public function init($options = '') + { + global $conf; + + $result = $this->_load_tables('/stundenzettel/sql/'); + if ($result < 0) { + return -1; + } + + // Extrafeld "Auftragsbeschreibung" für Aufträge anlegen + $this->createExtraFieldOrderDescription(); + + // View für Dienstleistungen erstellen (für sellist-Filter) + $this->createServicesView(); + + // Extrafeld "Standard-Leistung" für Kunden anlegen + $this->createExtraFieldDefaultService(); + + $sql = array(); + + return $this->_init($sql, $options); + } + + /** + * Erstellt das Extrafeld "Auftragsbeschreibung" für Aufträge (commande) + * + * @return int 1 if created or exists, -1 if error + */ + private function createExtraFieldOrderDescription() + { + global $langs; + + require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php'; + + $extrafields = new ExtraFields($this->db); + + // Prüfen ob Extrafeld bereits existiert + $extrafields->fetch_name_optionals_label('commande'); + + if (!isset($extrafields->attributes['commande']['label']['auftragsbeschreibung'])) { + // Extrafeld anlegen: Textarea für Auftragsbeschreibung + $result = $extrafields->addExtraField( + 'auftragsbeschreibung', // attrname - Feldname + 'Auftragsbeschreibung', // label - Anzeigename + 'text', // type - Feldtyp (text = Textarea) + 100, // pos - Position + '', // size - Größe (leer für text) + 'commande', // elementtype - Objekttyp + 0, // unique + 0, // required + '', // default_value + array('options' => array()), // param + 1, // alwayseditable + '', // perms + 1, // list - In Liste anzeigen + '', // ishidden + 0, // computed + '', // entity + '', // langfile + '', // enabled + 0, // totalizable + 0, // printable + array('css' => '', 'cssview' => '', 'csslist' => '') // moreparams + ); + + if ($result < 0) { + dol_syslog("modStundenzettel::createExtraFieldOrderDescription Error creating extrafield: ".$extrafields->error, LOG_ERR); + return -1; + } + + dol_syslog("modStundenzettel::createExtraFieldOrderDescription Extrafield 'auftragsbeschreibung' created successfully", LOG_DEBUG); + } + + return 1; + } + + /** + * Erstellt eine View für Dienstleistungen (fk_product_type = 1) + * Diese View wird für das sellist-Extrafeld verwendet + * + * @return int 1 if created, -1 if error + */ + private function createServicesView() + { + $sql = "CREATE OR REPLACE VIEW ".MAIN_DB_PREFIX."product_services AS + SELECT rowid, ref, label, description, fk_product_type, entity, tosell, tobuy + FROM ".MAIN_DB_PREFIX."product + WHERE fk_product_type = 1"; + + $resql = $this->db->query($sql); + if (!$resql) { + dol_syslog("modStundenzettel::createServicesView Error: ".$this->db->lasterror(), LOG_ERR); + return -1; + } + + dol_syslog("modStundenzettel::createServicesView View created successfully", LOG_DEBUG); + return 1; + } + + /** + * Erstellt das Extrafeld "Standard-Leistung" für Kunden (societe) + * + * @return int 1 if created or exists, -1 if error + */ + private function createExtraFieldDefaultService() + { + global $langs; + + require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php'; + + $extrafields = new ExtraFields($this->db); + + // Prüfen ob Extrafeld bereits existiert + $extrafields->fetch_name_optionals_label('societe'); + + if (!isset($extrafields->attributes['societe']['label']['stundenzettel_default_service'])) { + // Extrafeld anlegen: sellist mit View für nur Dienstleistungen + $result = $extrafields->addExtraField( + 'stundenzettel_default_service', // attrname - Feldname + 'Standard-Leistung (Stundenzettel)', // label - Anzeigename + 'sellist', // type - Feldtyp (sellist = SQL-basierte Auswahlliste) + 200, // pos - Position + '', // size - Größe + 'societe', // elementtype - Objekttyp (Kunden/Lieferanten) + 0, // unique + 0, // required + '', // default_value + array('options' => array('product_services:label:rowid' => null)), // param - View mit nur Dienstleistungen + 1, // alwayseditable + '', // perms + 1, // list - In Liste anzeigen + '', // ishidden + 0, // computed + '', // entity + '', // langfile + '$conf->stundenzettel->enabled', // enabled - nur wenn Modul aktiv + 0, // totalizable + 0, // printable + array('css' => '', 'cssview' => '', 'csslist' => '') // moreparams + ); + + if ($result < 0) { + dol_syslog("modStundenzettel::createExtraFieldDefaultService Error creating extrafield: ".$extrafields->error, LOG_ERR); + return -1; + } + + dol_syslog("modStundenzettel::createExtraFieldDefaultService Extrafield 'stundenzettel_default_service' created successfully", LOG_DEBUG); + } + + return 1; + } + + /** + * Funktion beim Deaktivieren des Moduls + * + * @param string $options Options when disabling module + * @return int 1 if OK, 0 if KO + */ + public function remove($options = '') + { + $sql = array(); + + return $this->_remove($sql, $options); + } +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..59c295a --- /dev/null +++ b/index.php @@ -0,0 +1,239 @@ + + * + * Stundenzettel - Dashboard/Übersicht + */ + +// 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.'/core/class/html.formfile.class.php'; +require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php'; + +// Load translation files +$langs->loadLangs(array("stundenzettel@stundenzettel", "orders")); + +// Security check +if (!$user->hasRight('stundenzettel', 'read')) { + accessforbidden(); +} + +$action = GETPOST('action', 'aZ09'); + +/* + * View + */ + +$form = new Form($db); + +// Linkes Menü aktivieren +$_GET['mainmenu'] = 'stundenzettel'; + +llxHeader('', $langs->trans("Stundenzettel"), '', '', 0, 0, '', '', '', 'mod-stundenzettel page-index'); + +print load_fiche_titre($langs->trans("Stundenzettel"), '', 'clock'); + +print '
'; + +// Statistik-Boxen +print '
'; + +// Box: Offene Stundenzettel +print '
'; +print ''; +print ''; +print ''; +print ''; + +$sql = "SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."stundenzettel WHERE status = 0 AND entity = ".((int)$conf->entity); +$resql = $db->query($sql); +if ($resql) { + $obj = $db->fetch_object($resql); + print ''; +} + +$sql = "SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."stundenzettel WHERE status = 1 AND entity = ".((int)$conf->entity); +$resql = $db->query($sql); +if ($resql) { + $obj = $db->fetch_object($resql); + print ''; +} + +print '
'.$langs->trans("BoxOpenStundenzettel").'
'.$langs->trans("StatusDraft").''.$obj->nb.'
'.$langs->trans("StatusValidated").''.$obj->nb.'
'; +print '
'; + +// Box: Schnellzugriff +print '
'; +print '
'; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print '
'.$langs->trans("Actions").'
'; +print ''; +print img_picto('', 'add', 'class="pictofixedwidth"').$langs->trans("CreateStundenzettel"); +print ''; +print '
'; +print ''; +print img_picto('', 'list', 'class="pictofixedwidth"').$langs->trans("StundenzettelList"); +print ''; +print '
'; +print '
'; + +print '
'; // fichethirdleft + +print '
'; + +// Zuletzt bearbeitete Stundenzettel +print '
'; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; + +$sql = "SELECT s.rowid, s.ref, s.date_stundenzettel, s.status, s.fk_commande, s.fk_soc,"; +$sql .= " c.ref as order_ref, soc.nom as soc_name"; +$sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel as s"; +$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."commande as c ON c.rowid = s.fk_commande"; +$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as soc ON soc.rowid = s.fk_soc"; +$sql .= " WHERE s.entity = ".((int)$conf->entity); +$sql .= " ORDER BY s.tms DESC"; +$sql .= " LIMIT 10"; + +$resql = $db->query($sql); +if ($resql) { + $num = $db->num_rows($resql); + if ($num > 0) { + while ($obj = $db->fetch_object($resql)) { + print ''; + + // Referenz + print ''; + + // Datum + print ''; + + // Auftrag + print ''; + + // Kunde + print ''; + + // Status + print ''; + + print ''; + } + } else { + print ''; + } +} else { + dol_print_error($db); +} + +print '
'.$langs->trans("BoxRecentStundenzettel").''.$langs->trans("StundenzettelDate").''.$langs->trans("StundenzettelOrder").''.$langs->trans("StundenzettelCustomer").''.$langs->trans("StundenzettelStatus").'
'; + print ''; + print img_picto('', 'clock', 'class="pictofixedwidth"').$obj->ref; + print ''; + print ''.dol_print_date($db->jdate($obj->date_stundenzettel), 'day').''; + if ($obj->order_ref) { + print ''; + print img_picto('', 'order', 'class="pictofixedwidth"').$obj->order_ref; + print ''; + } + print ''; + if ($obj->soc_name) { + print ''; + print img_picto('', 'company', 'class="pictofixedwidth"').$obj->soc_name; + print ''; + } + print ''; + if ($obj->status == 0) { + print ''.$langs->trans("StatusDraft").''; + } elseif ($obj->status == 1) { + print ''.$langs->trans("StatusValidated").''; + } elseif ($obj->status == 2) { + print ''.$langs->trans("StatusInvoiced").''; + } + print '
'.$langs->trans("NoOpenStundenzettel").'
'; +print '
'; + +// Offene Aufträge mit Stundenzettel-Funktion +print '
'; +print '
'; +print ''; +print ''; +print ''; +print ''; + +$sql = "SELECT c.rowid, c.ref, c.date_commande, c.total_ht, soc.nom as soc_name, soc.rowid as soc_id"; +$sql .= " FROM ".MAIN_DB_PREFIX."commande as c"; +$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as soc ON soc.rowid = c.fk_soc"; +$sql .= " WHERE c.entity = ".((int)$conf->entity); +$sql .= " AND c.fk_statut IN (1, 2)"; // Bestätigt oder in Bearbeitung +$sql .= " ORDER BY c.date_commande DESC"; +$sql .= " LIMIT 10"; + +$resql = $db->query($sql); +if ($resql) { + $num = $db->num_rows($resql); + if ($num > 0) { + while ($obj = $db->fetch_object($resql)) { + print ''; + + // Auftrag + print ''; + + // Kunde + print ''; + + // Datum + print ''; + + // Aktion + print ''; + + print ''; + } + } else { + print ''; + } +} + +print '
'.$langs->trans("OpenOrders").' - '.$langs->trans("ActivateStundenzettel").'
'; + print ''; + print img_picto('', 'order', 'class="pictofixedwidth"').$obj->ref; + print ''; + print ''; + print ''; + print $obj->soc_name; + print ''; + print ''.dol_print_date($db->jdate($obj->date_commande), 'day').''; + print ''; + print img_picto('', 'clock', 'class="pictofixedwidth"').$langs->trans("Stundenzettel"); + print ''; + print '
'.$langs->trans("NoRecordFound").'
'; +print '
'; + +print '
'; // fichetwothirdright + +print '
'; // fichecenter + +print '
'; + +llxFooter(); +$db->close(); diff --git a/langs/de_DE/stundenzettel.lang b/langs/de_DE/stundenzettel.lang new file mode 100644 index 0000000..6ea709f --- /dev/null +++ b/langs/de_DE/stundenzettel.lang @@ -0,0 +1,233 @@ +# Stundenzettel Modul - Deutsche Sprachdatei +# Copyright (C) 2026 Data IT Solution + +# Modul +Module500200Name = Stundenzettel +Module500200Desc = Stundenzettel-Verwaltung für Aufträge + +# Menü +Stundenzettel = Stundenzettel +StundenzettelList = Alle Stundenzettel +StundenzettelNew = Neuer Stundenzettel +StundenzettelOverview = Übersicht + +# Felder +StundenzettelRef = Stundenzettel-Nr. +StundenzettelDate = Datum +StundenzettelOrder = Auftrag +OrderDescription = Auftragsbeschreibung +StundenzettelCustomer = Kunde +StundenzettelStatus = Status +StundenzettelAuthor = Ersteller +StundenzettelValidatedBy = Freigegeben von + +# Status +StatusDraft = Entwurf +StatusValidated = Freigegeben +StatusInvoiced = In Rechnung übertragen +StatusCanceled = Storniert + +# Aktionen +CreateStundenzettel = Stundenzettel erstellen +EditStundenzettel = Stundenzettel bearbeiten +ValidateStundenzettel = Stundenzettel freigeben +DeleteStundenzettel = Stundenzettel löschen +CloneStundenzettel = Stundenzettel duplizieren +TransferToInvoice = In Rechnung übertragen +ActivateStundenzettel = Stundenzettel aktivieren +ReopenStundenzettel = Stundenzettel wieder öffnen + +# Leistungen +Leistungen = Leistungen +LeistungNew = Neue Leistung +LeistungDate = Datum +LeistungTimeStart = Beginn +LeistungTimeEnd = Ende +LeistungDuration = Dauer +LeistungDescription = Beschreibung +AddLeistung = Leistung hinzufügen + +# Produkte +Products = Produkte +ProductsDesc = Hier können Sie Produkte aus dem Auftrag erfassen, die verbraucht wurden. +ProductsFromOrder = Produkte aus Auftrag +ProductList = Produktliste +ProductsAdded = Hinzugefügte Produkte +OtherProducts = Sonstige Produkte +QtyOrdered = Beauftragt +QtyFromOrder = Beauftragt +QtyUsed = Erfasst +QtyDelivered = Verbaut +QtyDone = Erledigt +QtyRemaining = Verbleibend +QtyAdded = Hinzugefügt +QtyRemoved = Entfallen +AddProduct = Produkt hinzufügen +AddMoreProducts = Weitere Produkte hinzufügen +SelectProducts = Produkte auswählen +NoProductsInOrder = Keine Produkte im Auftrag +AllProductsUsedOrOmitted = Alle Produkte bereits verbaut oder als entfällt markiert +TransferProducts = Produkte übernehmen +TransferToStundenzettel = Übernehmen in Stundenzettel +FromOrder = Aus Auftrag +Added = Hinzugefügt +AlreadyOnStundenzettel = Bereits auf diesem Stundenzettel +BackToOrder = Zurück zum Auftrag +Add = Hinzufügen +FreeText = Freitext + +# Tabs +Card = Übersicht +View = Anzeigen + +# Tracking +DeliveryTracking = Lieferauflistung +TrackingStatus = Status +TrackingOpen = Offen +TrackingPartial = Teilweise erledigt +TrackingDone = Erledigt + +# Notizen +Notes = Notizen +NotesForNextVisit = Notizen für nächsten Termin +NotesMemo = Merkzettel +AddNote = Notiz hinzufügen +NoteText = Notiz +ConfirmDeleteNote = Diese Notiz wirklich löschen? +NoNotesYet = Noch keine Notizen vorhanden +OpenNotes = Offene Notizen +CheckedNotes = Erledigte Notizen +MarkAsDone = Als erledigt markieren +MarkAsOpen = Wieder öffnen + +# Meldungen +ConfirmValidate = Stundenzettel wirklich freigeben? +ConfirmDelete = Stundenzettel wirklich löschen? +ConfirmDeleteLeistung = Diese Leistung wirklich löschen? +RecordDeleted = Eintrag gelöscht +ConfirmTransfer = Alle Produkte in Rechnung übertragen? +AllProductsDocumented = Alle Produkte dokumentiert +NotAllProductsDocumented = Nicht alle Produkte dokumentiert +NoOpenStundenzettel = Keine offenen Stundenzettel +StundenzettelCreated = Stundenzettel erstellt +StundenzettelExistsForDate = Für diesen Auftrag und dieses Datum existiert bereits ein Stundenzettel +StundenzettelValidated = Stundenzettel freigegeben +StundenzettelDeleted = Stundenzettel gelöscht + +# Mengenwarnung +ConfirmExceedOrderQty = Auftragsmenge wird überschritten! +OrderQty = Auftragsmenge +TotalUsedAllStz = Gesamtmenge alle Stundenzettel +ConfirmAdditionalWork = Mehraufwand bestätigen? + +# Mehraufwand +Mehraufwand = Mehraufwand +MehraufwandDesc = Hier können Sie zusätzliche Produkte erfassen, die nicht im ursprünglichen Auftrag enthalten waren. +AddMehraufwand = Mehraufwand hinzufügen +OrFreeText = Oder Freitext +FreeTextDescription = Freitext-Beschreibung +ConfirmDeleteMehraufwand = Diesen Mehraufwand wirklich löschen? +ErrorNoProductSelected = Bitte wählen Sie ein Produkt oder geben Sie eine Beschreibung ein +AdditionalQty = Zusätzliche Menge +FromStundenzettel = Aus Stundenzettel +ProductExistsInOrder = Produkt im Auftrag gefunden, als Verbrauch hinzugefügt +MehraufwandConflictTitle = Produkt bereits als Mehraufwand vorhanden +MehraufwandConflictWarning = Folgende Produkte sind bereits als Mehraufwand erfasst +MehraufwandConflictQuestion = Möchten Sie diese Produkte trotzdem als Verbrauch übernehmen? +MehraufwandConflictExplanation = Wenn Sie fortfahren, wird das Produkt sowohl als Auftragsposition (Verbrauch) als auch als Mehraufwand geführt. Dies kann zu doppelter Berechnung führen. +MehraufwandTransferTitle = Mehraufwand übernehmen +MehraufwandTransferWarning = Folgende Produkte wurden als Mehraufwand erfasst +MehraufwandTransferQuestion = Diese Produkte werden als normale Verbrauchsposition übernommen. +MehraufwandTransferExplanation = Die Produkte werden in die Produktliste des Stundenzettels übernommen (nicht als Mehraufwand). Die ursprüngliche Mehraufwand-Zeile bleibt bestehen. + +# Entfällt +Entfaellt = Entfällt +EntfaelltDesc = Hier können Sie Produkte aus dem Auftrag erfassen, die nicht verbaut werden müssen. +AddEntfaellt = Entfällt hinzufügen +Reason = Grund +Optional = optional +ConfirmDeleteEntfaellt = Diesen Eintrag wirklich löschen? + +# Fehler +ErrorNoOrder = Kein Auftrag ausgewählt +ErrorOrderNotFound = Auftrag nicht gefunden +ErrorNoProducts = Keine Produkte ausgewählt +ErrorAlreadyValidated = Stundenzettel bereits freigegeben +ErrorQtyExceedsAvailable = Menge überschreitet verfügbare Menge (max. %s) +ErrorTimeOverlap = Zeitüberschneidung mit bestehender Leistung (%s - %s) +ErrorStundenzettelReleased = Die Stundenzettel für diesen Auftrag wurden bereits freigegeben und können nicht mehr geändert werden + +# Widgets +BoxRecentStundenzettel = Zuletzt bearbeitete Stundenzettel +BoxOpenStundenzettel = Offene Stundenzettel + +# Berechtigungen +PermissionRead = Stundenzettel lesen +PermissionWrite = Stundenzettel erstellen/bearbeiten +PermissionValidate = Stundenzettel freigeben +PermissionDelete = Stundenzettel löschen + +# Einstellungen +StundenzettelSetup = Stundenzettel Einstellungen +TimeInputMode = Zeiteingabe-Modus +TimeInputDropdown = Dropdown (15-Minuten-Takt) +TimeInputText = Freitext (exakte Uhrzeit) +PreciseTimeEntry = Exakte Zeiterfassung +DefaultFilter = Standard-Filter Produktliste +DefaultFilterDesc = Welcher Filter soll standardmäßig in der Produktliste angezeigt werden? +DefaultDate = Standard-Datum für Stundenzettel +DefaultDateDesc = Welches Datum soll standardmäßig für neue Stundenzettel vorgeschlagen werden? +DateToday = Immer aktuelles Datum +DateLastOpen = Datum des letzten offenen Stundenzettels +NewStundenzettelForToday = Neuer Stundenzettel für heute +SelectedStundenzettelNotToday = Der ausgewählte Stundenzettel %s ist vom %s - es wird ein neuer für heute erstellt + +# Filter +Filter = Filter +FilterAll = Alle +FilterOpen = Offen +FilterDone = Erledigt + +# Sections +ExpandAll = Alles ausklappen +CollapseAll = Alles einklappen +ShowDetails = Details anzeigen +HideDetails = Details verbergen +Type = Typ + +# Freigabe und Rechnung +ReleaseStundenzettel = Stundenzettel freigeben +ReopenStundenzettel = Stundenzettel wiedereröffnen +TransferToInvoice = In Rechnung übernehmen +StundenzettelReleased = Stundenzettel wurden freigegeben +StundenzettelReopened = Stundenzettel wurden wiedereröffnet +AllStundenzettelMustBeValidated = Alle Stundenzettel müssen erst validiert werden +RemainingProductsWarning = Folgende Produkte sind noch nicht vollständig dokumentiert +ConfirmReleaseWithRemainingTitle = Freigabe mit offenen Produkten? +ConfirmReleaseWithRemaining = Es gibt noch nicht vollständig dokumentierte Produkte. Wollen Sie die Stundenzettel trotzdem freigeben? +ConfirmReleaseFinalTitle = Endgültige Freigabe bestätigen +ConfirmReleaseFinal = Sind Sie WIRKLICH sicher? Nach der Freigabe können keine Änderungen mehr vorgenommen werden. +ConfirmTransferInvoiceTitle = In Rechnung übernehmen? +ConfirmTransferInvoice = Alle dokumentierten Produkte werden in eine neue Rechnung übertragen. Fortfahren? +InvoiceCreated = Rechnung wurde erstellt +CreatedFromStundenzettel = Erstellt aus Stundenzettel +StundenzettelTransferredToInvoice = Stundenzettel wurden bereits in Rechnung übertragen +StundenzettelReleasedNoChanges = Die Stundenzettel sind freigegeben. Keine Änderungen mehr möglich. +ResetStundenzettel = Status zurücksetzen +StundenzettelReset = Stundenzettel-Status wurde zurückgesetzt + +# Standard-Leistung +DefaultService = Standard-Leistung +DefaultServiceDesc = Standard-Dienstleistung für Stundenzettel (wird beim Kunden hinterlegt) +DefaultServiceFromCustomer = Standard-Leistung vom Kunden +NoDefaultServiceSet = Keine Standard-Leistung hinterlegt +SetDefaultServiceInCustomer = Standard-Leistung beim Kunden hinterlegen +PlannedHours = Geplante Stunden +TotalHours = Gesamtstunden +HoursFromOrder = Stunden aus Auftrag + +# Rechnungsübernahme Stunden +InvoiceHoursMode = Stunden-Übernahme +InvoiceHoursModeTotal = Gesamtstunden auf einer Zeile +InvoiceHoursModePerDay = Pro Tag eine Zeile +SelectInvoiceHoursMode = Wie sollen die Arbeitsstunden übernommen werden? diff --git a/lib/stundenzettel.lib.php b/lib/stundenzettel.lib.php new file mode 100644 index 0000000..78fb556 --- /dev/null +++ b/lib/stundenzettel.lib.php @@ -0,0 +1,89 @@ + + * + * Stundenzettel - Library functions + */ + +/** + * Prepare array of tabs for Stundenzettel card + * + * @param Stundenzettel $object Object + * @return array Array of tabs + */ +function stundenzettel_prepare_head($object) +{ + global $db, $langs, $conf, $user; + + $langs->load("stundenzettel@stundenzettel"); + + $h = 0; + $head = array(); + + // Tab 1: Kundenauftrag (Link zum Auftrag) - immer am Anfang + if ($object->fk_commande > 0) { + $head[$h][0] = DOL_URL_ROOT.'/commande/card.php?id='.$object->fk_commande; + $head[$h][1] = $langs->trans("Order"); + $head[$h][2] = 'order'; + $h++; + } + + // Tab 2: Produktliste (Link zu stundenzettel_commande.php mit Produktliste aus Auftrag) + if ($object->fk_commande > 0) { + $head[$h][0] = dol_buildpath('/stundenzettel/stundenzettel_commande.php', 1).'?id='.$object->fk_commande.'&tab=products&noredirect=1&stundenzettel_id='.$object->id; + $head[$h][1] = $langs->trans("ProductList"); + $head[$h][2] = 'productlist'; + $h++; + } + + // Tab 3: Stundenzettel (Link zum aktiven Stundenzettel - card.php) + $nbLeistungen = 0; + if (!empty($object->leistungen)) { + $nbLeistungen = count($object->leistungen); + } + $nbProducts = 0; + if (!empty($object->products)) { + $nbProducts = count($object->products); + } + $head[$h][0] = dol_buildpath('/stundenzettel/card.php', 1).'?id='.$object->id; + $head[$h][1] = $langs->trans("Stundenzettel"); + $totalItems = $nbLeistungen + $nbProducts; + if ($totalItems > 0) { + $head[$h][1] .= ''.$totalItems.''; + } + $head[$h][2] = 'card'; + $h++; + + // Tab 4: Alle Stundenzettel (Liste aller Stundenzettel für diesen Auftrag) + if ($object->fk_commande > 0) { + // Anzahl Stundenzettel für diesen Auftrag zählen + $sql = "SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."stundenzettel WHERE fk_commande = ".((int)$object->fk_commande); + $resql = $db->query($sql); + $nbStundenzettel = 0; + if ($resql && ($obj = $db->fetch_object($resql))) { + $nbStundenzettel = $obj->nb; + } + $head[$h][0] = dol_buildpath('/stundenzettel/stundenzettel_commande.php', 1).'?id='.$object->fk_commande.'&tab=stundenzettel&noredirect=1&stundenzettel_id='.$object->id; + $head[$h][1] = $langs->trans("StundenzettelList"); + if ($nbStundenzettel > 0) { + $head[$h][1] .= ''.$nbStundenzettel.''; + } + $head[$h][2] = 'stundenzettel_list'; + $h++; + } + + // Tab 5: Lieferauflistung (Tracking) + if ($object->fk_commande > 0) { + $head[$h][0] = dol_buildpath('/stundenzettel/stundenzettel_commande.php', 1).'?id='.$object->fk_commande.'&tab=tracking&noredirect=1&stundenzettel_id='.$object->id; + $head[$h][1] = $langs->trans("DeliveryTracking"); + $head[$h][2] = 'tracking'; + $h++; + } + + // Tab 6: Notizen + $head[$h][0] = dol_buildpath('/stundenzettel/card.php', 1).'?id='.$object->id.'&tab=notes'; + $head[$h][1] = $langs->trans("Notes"); + $head[$h][2] = 'notes'; + $h++; + + return $head; +} diff --git a/list.php b/list.php new file mode 100644 index 0000000..ec65a0d --- /dev/null +++ b/list.php @@ -0,0 +1,216 @@ + + * + * Stundenzettel - Liste + */ + +// 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.'/core/class/html.formother.class.php'; +dol_include_once('/stundenzettel/class/stundenzettel.class.php'); + +// Load translation files +$langs->loadLangs(array("stundenzettel@stundenzettel", "orders")); + +// Security check +if (!$user->hasRight('stundenzettel', 'read')) { + accessforbidden(); +} + +// Get parameters +$action = GETPOST('action', 'aZ09'); +$massaction = GETPOST('massaction', 'alpha'); +$contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'stundenzettellist'; + +$search_ref = GETPOST('search_ref', 'alpha'); +$search_order = GETPOST('search_order', 'alpha'); +$search_soc = GETPOST('search_soc', 'alpha'); +$search_status = GETPOST('search_status', 'int'); +$search_author = GETPOST('search_author', 'int'); + +// __USER_ID__ ersetzen falls verwendet +if ($search_author == '__USER_ID__' || GETPOST('search_author', 'alpha') == '__USER_ID__') { + $search_author = $user->id; +} + +$sortfield = GETPOST('sortfield', 'aZ09comma'); +$sortorder = GETPOST('sortorder', 'aZ09comma'); +$page = GETPOSTISSET('pageplusone') ? (GETPOST('pageplusone') - 1) : GETPOST("page", 'int'); +if (empty($page) || $page < 0) $page = 0; +$limit = GETPOST('limit', 'int') ? GETPOST('limit', 'int') : $conf->liste_limit; +$offset = $limit * $page; + +if (!$sortfield) $sortfield = 's.date_stundenzettel'; +if (!$sortorder) $sortorder = 'DESC'; + +// Build SQL +$sql = "SELECT s.rowid, s.ref, s.date_stundenzettel, s.status, s.fk_commande, s.fk_soc,"; +$sql .= " c.ref as order_ref, soc.nom as soc_name"; +$sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel as s"; +$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."commande as c ON c.rowid = s.fk_commande"; +$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as soc ON soc.rowid = s.fk_soc"; +$sql .= " WHERE s.entity = ".((int)$conf->entity); + +if ($search_ref) { + $sql .= natural_search('s.ref', $search_ref); +} +if ($search_order) { + $sql .= natural_search('c.ref', $search_order); +} +if ($search_soc) { + $sql .= natural_search('soc.nom', $search_soc); +} +if ($search_status !== '' && $search_status >= 0) { + $sql .= " AND s.status = ".((int)$search_status); +} +if ($search_author > 0) { + $sql .= " AND s.fk_user_author = ".((int)$search_author); +} + +$sql .= $db->order($sortfield, $sortorder); + +// Count total +$nbtotalofrecords = ''; +$resql = $db->query($sql); +if ($resql) { + $nbtotalofrecords = $db->num_rows($resql); +} + +$sql .= $db->plimit($limit + 1, $offset); + +/* + * View + */ + +$form = new Form($db); +$formother = new FormOther($db); +$objectstatic = new Stundenzettel($db); + +$title = $langs->trans("StundenzettelList"); + +// Linkes Menü aktivieren +$_GET['mainmenu'] = 'stundenzettel'; + +llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-stundenzettel page-list'); + +$param = ''; +if ($search_ref) $param .= '&search_ref='.urlencode($search_ref); +if ($search_order) $param .= '&search_order='.urlencode($search_order); +if ($search_soc) $param .= '&search_soc='.urlencode($search_soc); +if ($search_status !== '') $param .= '&search_status='.urlencode($search_status); +if ($search_author > 0) $param .= '&search_author='.urlencode($search_author); + +print '
'; +print ''; + +$newcardbutton = ''; +if ($user->hasRight('stundenzettel', 'write')) { + $newcardbutton = ''; + $newcardbutton .= ''; + $newcardbutton .= ''; +} + +print_barre_liste($title, $page, $_SERVER['PHP_SELF'], $param, $sortfield, $sortorder, '', $nbtotalofrecords, $nbtotalofrecords, 'clock', 0, $newcardbutton, '', $limit); + +print '
'; +print ''; + +// Header +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; + +print ''; +print_liste_field_titre("Ref", $_SERVER['PHP_SELF'], "s.ref", "", $param, "", $sortfield, $sortorder); +print_liste_field_titre("Date", $_SERVER['PHP_SELF'], "s.date_stundenzettel", "", $param, "", $sortfield, $sortorder); +print_liste_field_titre("Order", $_SERVER['PHP_SELF'], "c.ref", "", $param, "", $sortfield, $sortorder); +print_liste_field_titre("Customer", $_SERVER['PHP_SELF'], "soc.nom", "", $param, "", $sortfield, $sortorder); +print_liste_field_titre("Status", $_SERVER['PHP_SELF'], "s.status", "", $param, "", $sortfield, $sortorder, 'center '); +print_liste_field_titre("", $_SERVER['PHP_SELF'], "", "", $param, "", $sortfield, $sortorder, 'center '); +print ''; + +$resql = $db->query($sql); +if ($resql) { + $num = $db->num_rows($resql); + $i = 0; + + while ($i < min($num, $limit)) { + $obj = $db->fetch_object($resql); + + print ''; + + // Ref + print ''; + + // Datum + print ''; + + // Auftrag + print ''; + + // Kunde + print ''; + + // Status + print ''; + + // Actions + print ''; + + print ''; + $i++; + } + + if ($num == 0) { + print ''; + } + + $db->free($resql); +} else { + dol_print_error($db); +} + +print '
'; +print ''; +print ''; +print ''; +print ''; +print '
'; + print ''; + print img_picto('', 'clock', 'class="pictofixedwidth"').$obj->ref; + print ''; + print ''.dol_print_date($db->jdate($obj->date_stundenzettel), 'day').''; + if ($obj->order_ref) { + print ''; + print img_picto('', 'order', 'class="pictofixedwidth"').$obj->order_ref; + print ''; + } + print ''; + if ($obj->soc_name) { + print ''; + print $obj->soc_name; + print ''; + } + print ''.$objectstatic->LibStatut($obj->status, 5).''; + print ''.img_picto($langs->trans("View"), 'eye').''; + print '
'.$langs->trans("NoRecordFound").'
'; +print '
'; +print '
'; + +llxFooter(); +$db->close(); diff --git a/sql/dolibarr_allversions.sql b/sql/dolibarr_allversions.sql new file mode 100644 index 0000000..5026bb4 --- /dev/null +++ b/sql/dolibarr_allversions.sql @@ -0,0 +1,3 @@ +-- +-- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version. +-- diff --git a/sql/llx_commande_extrafields_stundenzettel.sql b/sql/llx_commande_extrafields_stundenzettel.sql new file mode 100644 index 0000000..3273e23 --- /dev/null +++ b/sql/llx_commande_extrafields_stundenzettel.sql @@ -0,0 +1,14 @@ +-- ============================================================================ +-- Extrafeld für Stundenzettel-Status am Auftrag +-- 0 = Offen (Stundenzettel können bearbeitet werden) +-- 1 = Freigegeben (Stundenzettel gesperrt, bereit für Rechnung) +-- 2 = In Rechnung übertragen +-- ============================================================================ + +-- Extrafeld in extrafields-Tabelle registrieren +INSERT INTO llx_extrafields (name, entity, elementtype, label, type, size, fieldunique, fieldrequired, pos, alwayseditable, perms, langs, list, printable, fielddefault, fieldcomputed, fk_user_author, fk_user_modif, datec, enabled, help) +VALUES ('stundenzettel_status', 1, 'commande', 'StundenzettelStatus', 'int', '1', 0, 0, 100, 0, '', '', 0, 0, '0', '', NULL, NULL, NOW(), '1', '') +ON DUPLICATE KEY UPDATE label = 'StundenzettelStatus'; + +-- Spalte in commande_extrafields hinzufügen falls nicht vorhanden +ALTER TABLE llx_commande_extrafields ADD COLUMN IF NOT EXISTS stundenzettel_status INT DEFAULT 0; diff --git a/sql/llx_product_services.sql b/sql/llx_product_services.sql new file mode 100644 index 0000000..86c3c92 --- /dev/null +++ b/sql/llx_product_services.sql @@ -0,0 +1,9 @@ +-- ============================================================================ +-- View for services only (fk_product_type = 1) +-- Used for the "Standard-Leistung" extrafield dropdown on customers +-- ============================================================================ + +CREATE OR REPLACE VIEW llx_product_services AS +SELECT rowid, ref, label, description, fk_product_type, entity, tosell, tobuy +FROM llx_product +WHERE fk_product_type = 1; diff --git a/sql/llx_stundenzettel.key.sql b/sql/llx_stundenzettel.key.sql new file mode 100644 index 0000000..4d33d78 --- /dev/null +++ b/sql/llx_stundenzettel.key.sql @@ -0,0 +1 @@ +-- Keys for llx_stundenzettel (already defined in main SQL) diff --git a/sql/llx_stundenzettel.sql b/sql/llx_stundenzettel.sql new file mode 100644 index 0000000..74340fc --- /dev/null +++ b/sql/llx_stundenzettel.sql @@ -0,0 +1,40 @@ +-- ============================================================================ +-- Stundenzettel Haupttabelle +-- ============================================================================ + +CREATE TABLE llx_stundenzettel ( + rowid INTEGER AUTO_INCREMENT PRIMARY KEY, + ref VARCHAR(30) NOT NULL, -- Stundenzettel-Nummer (SZ2026-00001) + entity INTEGER DEFAULT 1 NOT NULL, + + -- Verknüpfungen + fk_commande INTEGER NOT NULL, -- Verknüpfung zum Auftrag + fk_facture INTEGER DEFAULT NULL, -- Verknüpfung zur Rechnung (nach Übertrag) + fk_soc INTEGER NOT NULL, -- Kunde + fk_user_author INTEGER NOT NULL, -- Ersteller + fk_user_valid INTEGER DEFAULT NULL, -- Wer hat freigegeben + + -- Datum + date_stundenzettel DATE NOT NULL, -- Datum des Stundenzettels + datec DATETIME, -- Erstelldatum + date_valid DATETIME DEFAULT NULL, -- Freigabedatum + + -- Status: 0=Entwurf, 1=Freigegeben, 2=In Rechnung übertragen, 9=Storniert + status TINYINT DEFAULT 0 NOT NULL, + + -- Notizen + note_private TEXT, + note_public TEXT, + + -- Technisch + import_key VARCHAR(14), + model_pdf VARCHAR(255), + tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- Indizes + UNIQUE KEY uk_stundenzettel_ref (ref, entity), + INDEX idx_stundenzettel_commande (fk_commande), + INDEX idx_stundenzettel_soc (fk_soc), + INDEX idx_stundenzettel_date (date_stundenzettel), + INDEX idx_stundenzettel_status (status) +) ENGINE=InnoDB; diff --git a/sql/llx_stundenzettel_leistung.key.sql b/sql/llx_stundenzettel_leistung.key.sql new file mode 100644 index 0000000..478cc82 --- /dev/null +++ b/sql/llx_stundenzettel_leistung.key.sql @@ -0,0 +1 @@ +-- Keys for llx_stundenzettel_leistung (already defined in main SQL) diff --git a/sql/llx_stundenzettel_leistung.sql b/sql/llx_stundenzettel_leistung.sql new file mode 100644 index 0000000..2e59e37 --- /dev/null +++ b/sql/llx_stundenzettel_leistung.sql @@ -0,0 +1,34 @@ +-- ============================================================================ +-- Stundenzettel Leistungen (Arbeitszeiten) +-- Mehrere Leistungen pro Stundenzettel möglich +-- ============================================================================ + +CREATE TABLE llx_stundenzettel_leistung ( + rowid INTEGER AUTO_INCREMENT PRIMARY KEY, + fk_stundenzettel INTEGER NOT NULL, -- Verknüpfung zum Stundenzettel + fk_user INTEGER DEFAULT NULL, -- Welcher Mitarbeiter + + -- Zeitraum + date_leistung DATE NOT NULL, -- Datum der Leistung + time_start TIME DEFAULT NULL, -- Startzeit + time_end TIME DEFAULT NULL, -- Endzeit + duration INTEGER DEFAULT 0, -- Dauer in Minuten (berechnet oder manuell) + + -- Beschreibung + description TEXT, -- Was wurde gemacht + + -- Rang für Sortierung + rang INTEGER DEFAULT 0, + + -- Technisch + tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- Indizes + INDEX idx_leistung_stundenzettel (fk_stundenzettel), + INDEX idx_leistung_date (date_leistung), + INDEX idx_leistung_user (fk_user), + + -- Foreign Key + CONSTRAINT fk_leistung_stundenzettel FOREIGN KEY (fk_stundenzettel) + REFERENCES llx_stundenzettel(rowid) ON DELETE CASCADE +) ENGINE=InnoDB; diff --git a/sql/llx_stundenzettel_note.sql b/sql/llx_stundenzettel_note.sql new file mode 100644 index 0000000..f787eda --- /dev/null +++ b/sql/llx_stundenzettel_note.sql @@ -0,0 +1,29 @@ +-- ============================================================================ +-- Stundenzettel Notizen (abhakbare Merkzettel) +-- Mehrere Notizen pro Stundenzettel möglich +-- ============================================================================ + +CREATE TABLE llx_stundenzettel_note ( + rowid INTEGER AUTO_INCREMENT PRIMARY KEY, + fk_stundenzettel INTEGER NOT NULL, -- Verknüpfung zum Stundenzettel + fk_user INTEGER DEFAULT NULL, -- Wer hat erstellt + + -- Inhalt + note TEXT NOT NULL, -- Die Notiz selbst + checked TINYINT(1) DEFAULT 0, -- 0=offen, 1=abgehakt + + -- Rang für Sortierung + rang INTEGER DEFAULT 0, + + -- Technisch + datec DATETIME DEFAULT NULL, -- Erstellt am + tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- Indizes + INDEX idx_note_stundenzettel (fk_stundenzettel), + INDEX idx_note_checked (checked), + + -- Foreign Key + CONSTRAINT fk_note_stundenzettel FOREIGN KEY (fk_stundenzettel) + REFERENCES llx_stundenzettel(rowid) ON DELETE CASCADE +) ENGINE=InnoDB; diff --git a/sql/llx_stundenzettel_product.key.sql b/sql/llx_stundenzettel_product.key.sql new file mode 100644 index 0000000..4d02321 --- /dev/null +++ b/sql/llx_stundenzettel_product.key.sql @@ -0,0 +1 @@ +-- Keys for llx_stundenzettel_product (already defined in main SQL) diff --git a/sql/llx_stundenzettel_product.sql b/sql/llx_stundenzettel_product.sql new file mode 100644 index 0000000..d0155be --- /dev/null +++ b/sql/llx_stundenzettel_product.sql @@ -0,0 +1,43 @@ +-- ============================================================================ +-- Stundenzettel Produkte +-- Welche Produkte wurden verbaut/geliefert +-- ============================================================================ + +CREATE TABLE llx_stundenzettel_product ( + rowid INTEGER AUTO_INCREMENT PRIMARY KEY, + fk_stundenzettel INTEGER NOT NULL, -- Verknüpfung zum Stundenzettel + + -- Produkt-Referenz + fk_product INTEGER DEFAULT NULL, -- Produkt-ID (kann NULL sein bei freien Zeilen) + fk_commandedet INTEGER DEFAULT NULL, -- Original-Zeile aus Auftrag + fk_manager_line INTEGER DEFAULT NULL, -- Zeile aus llx_facture_lines_manager (für Produktgruppen) + + -- Produktdaten (Kopie für Historie) + product_ref VARCHAR(128), -- Produktreferenz + product_label VARCHAR(255), -- Produktbezeichnung + description TEXT, -- Beschreibung + + -- Mengen + qty_original DECIMAL(24,8) DEFAULT 0, -- Ursprüngliche Menge aus Auftrag + qty_done DECIMAL(24,8) DEFAULT 0, -- Erledigte/verbaute Menge auf diesem Zettel + qty_cumulated DECIMAL(24,8) DEFAULT 0, -- Kumuliert über alle Stundenzettel + + -- Herkunft: 'order' = aus Auftrag, 'added' = manuell hinzugefügt + origin VARCHAR(20) DEFAULT 'order', + + -- Rang für Sortierung + rang INTEGER DEFAULT 0, + + -- Technisch + tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- Indizes + INDEX idx_szproduct_stundenzettel (fk_stundenzettel), + INDEX idx_szproduct_product (fk_product), + INDEX idx_szproduct_commandedet (fk_commandedet), + INDEX idx_szproduct_manager (fk_manager_line), + + -- Foreign Key + CONSTRAINT fk_szproduct_stundenzettel FOREIGN KEY (fk_stundenzettel) + REFERENCES llx_stundenzettel(rowid) ON DELETE CASCADE +) ENGINE=InnoDB; diff --git a/sql/llx_stundenzettel_tracking.key.sql b/sql/llx_stundenzettel_tracking.key.sql new file mode 100644 index 0000000..48d18ee --- /dev/null +++ b/sql/llx_stundenzettel_tracking.key.sql @@ -0,0 +1 @@ +-- Keys for llx_stundenzettel_tracking (already defined in main SQL) diff --git a/sql/llx_stundenzettel_tracking.sql b/sql/llx_stundenzettel_tracking.sql new file mode 100644 index 0000000..3ffde3e --- /dev/null +++ b/sql/llx_stundenzettel_tracking.sql @@ -0,0 +1,37 @@ +-- ============================================================================ +-- Stundenzettel Tracking +-- Gesamtübersicht der Mengen pro Auftrag (aggregiert über alle Stundenzettel) +-- ============================================================================ + +CREATE TABLE llx_stundenzettel_tracking ( + rowid INTEGER AUTO_INCREMENT PRIMARY KEY, + fk_commande INTEGER NOT NULL, -- Auftrag + + -- Produkt-Referenz + fk_product INTEGER DEFAULT NULL, -- Produkt-ID + fk_commandedet INTEGER DEFAULT NULL, -- Original-Zeile aus Auftrag + fk_manager_line INTEGER DEFAULT NULL, -- Zeile aus llx_facture_lines_manager + + -- Produktdaten + product_ref VARCHAR(128), + product_label VARCHAR(255), + + -- Mengen + qty_ordered DECIMAL(24,8) DEFAULT 0, -- Bestellte Menge (aus Auftrag) + qty_delivered DECIMAL(24,8) DEFAULT 0, -- Gelieferte Menge (Summe aller Stundenzettel) + qty_added DECIMAL(24,8) DEFAULT 0, -- Zusätzlich hinzugefügt + qty_removed DECIMAL(24,8) DEFAULT 0, -- Entfallen/Storniert + qty_remaining DECIMAL(24,8) DEFAULT 0, -- Verbleibend (berechnet) + + -- Status: 'open' = offen, 'partial' = teilweise, 'done' = erledigt + status VARCHAR(20) DEFAULT 'open', + + -- Technisch + tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- Indizes + UNIQUE KEY uk_tracking_line (fk_commande, fk_commandedet), + INDEX idx_tracking_commande (fk_commande), + INDEX idx_tracking_product (fk_product), + INDEX idx_tracking_status (status) +) ENGINE=InnoDB; diff --git a/stundenzettel_commande.php b/stundenzettel_commande.php new file mode 100644 index 0000000..86327e8 --- /dev/null +++ b/stundenzettel_commande.php @@ -0,0 +1,2325 @@ + + * + * Stundenzettel - Auftrags-Integration + * Zeigt Produktliste aus Auftrag mit Übernahme-Möglichkeit + */ + +// Load Dolibarr environment +$res = 0; +if (!$res && file_exists("../main.inc.php")) $res = @include "../main.inc.php"; +if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php"; +if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php"; +if (!$res) die("Include of main fails"); + +require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/order.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; +require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php'; +dol_include_once('/stundenzettel/class/stundenzettel.class.php'); + +// Load translation files +$langs->loadLangs(array("stundenzettel@stundenzettel", "orders", "products")); + +/** + * Formatiert Mengen: Ganzzahlen ohne Dezimalstellen, sonst max. 2 Stellen + * @param float $qty Menge + * @return string Formatierte Menge + */ +function formatQty($qty) { + $qty = (float)$qty; + if ($qty == floor($qty)) { + return number_format($qty, 0, ',', '.'); + } + // Runde auf 2 Stellen und entferne trailing zeros + $formatted = rtrim(rtrim(number_format($qty, 2, ',', '.'), '0'), ','); + return $formatted; +} + +// Get parameters +$id = GETPOST('id', 'int'); +$action = GETPOST('action', 'aZ09'); +$tab = GETPOST('tab', 'aZ09') ?: 'products'; +$stundenzettel_id = GETPOST('stundenzettel_id', 'int'); +$defaultFilter = getDolGlobalString('STUNDENZETTEL_DEFAULT_FILTER', 'open'); +$filter = GETPOST('filter', 'alpha') ?: $defaultFilter; // Filter: all, open, done + +// Security check +if (!$user->hasRight('stundenzettel', 'read')) { + accessforbidden(); +} + +// Load order +$order = new Commande($db); +if ($order->fetch($id) <= 0) { + dol_print_error($db, 'Order not found'); + exit; +} + +// Berechtigung: Nur zugewiesener Benutzer oder Admin +$canAccess = ($order->fk_user_author == $user->id || $user->admin || $user->hasRight('commande', 'lire')); +if (!$canAccess) { + accessforbidden('You are not assigned to this order'); +} + +// Auto-Redirect: Wenn ein aktiver Stundenzettel für heute existiert, dorthin weiterleiten +// Nur bei erstem Aufruf +$noRedirect = GETPOST('noredirect', 'int'); +if (!$noRedirect && empty($action)) { + // Suche nach offenem Stundenzettel für diesen Auftrag (heute oder generell offen) + $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel"; + $sql .= " WHERE fk_commande = ".((int)$order->id); + $sql .= " AND status = 0"; // Nur Entwürfe + $sql .= " ORDER BY date_stundenzettel DESC, rowid DESC"; + $sql .= " LIMIT 1"; + + $resql = $db->query($sql); + if ($resql && $db->num_rows($resql) > 0) { + $obj = $db->fetch_object($resql); + // Weiterleitung zum gefundenen Stundenzettel + header('Location: '.dol_buildpath('/stundenzettel/card.php?id='.$obj->rowid.'&tab=products', 1)); + exit; + } + // Kein Stundenzettel vorhanden - bleibe auf dieser Seite um einen zu erstellen +} + +/* + * Actions + */ + +// Produkte auf Stundenzettel übernehmen +if ($action == 'transfer_products' && $user->hasRight('stundenzettel', 'write')) { + // Prüfe ob Auftrag freigegeben ist + $order->fetch_optionals(); + $stzStatus = isset($order->array_options['options_stundenzettel_status']) ? (int)$order->array_options['options_stundenzettel_status'] : 0; + if ($stzStatus >= 1) { + setEventMessages($langs->trans('ErrorStundenzettelReleased'), null, 'errors'); + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1'); + exit; + } + + $selected = GETPOST('selected', 'array'); + $selected_mehraufwand = GETPOST('selected_mehraufwand', 'array'); + $date_stundenzettel = GETPOST('date_stundenzettel', 'alpha'); + $target_stundenzettel_id = GETPOST('stundenzettel_id', 'int'); // Vorausgewählter Stundenzettel + + // Prüfe ob mindestens etwas ausgewählt wurde + if (empty($selected) && empty($selected_mehraufwand)) { + setEventMessages($langs->trans('NoProductsSelected'), null, 'errors'); + } else { + // Wenn nur Mehraufwand ausgewählt, $selected als leeres Array initialisieren + if (empty($selected)) { + $selected = array(); + } + + // Mehraufwand-Zeilen werden direkt übernommen (ohne Bestätigungs-Dialog) + + // Produkte werden direkt übernommen (ohne Bestätigungs-Dialoge) + $stundenzettel = new Stundenzettel($db); + $use_stundenzettel_id = 0; + $forceNewStundenzettel = GETPOST('force_new_stundenzettel', 'int'); + + // Wenn ein Stundenzettel vorgegeben wurde, prüfen ob er von heute ist + if ($target_stundenzettel_id > 0 && !$forceNewStundenzettel) { + if ($stundenzettel->fetch($target_stundenzettel_id) > 0) { + // Prüfe ob der Stundenzettel von heute ist + $stzDateStr = date('Y-m-d', $stundenzettel->date_stundenzettel); + $todayStr = date('Y-m-d'); + if ($stzDateStr == $todayStr) { + // Stundenzettel ist von heute - verwenden + $use_stundenzettel_id = $target_stundenzettel_id; + } + // Sonst: neuen für heute erstellen (weiter unten) + } + } + + // Wenn kein passender Stundenzettel, nach Datum suchen oder neu erstellen + if ($use_stundenzettel_id <= 0) { + // Bei force_new oder wenn Stundenzettel von anderem Tag: heutiges Datum verwenden + $date = ($forceNewStundenzettel || !$date_stundenzettel) ? dol_now() : $date_stundenzettel; + + // Existierenden Stundenzettel für diesen Tag suchen oder neuen erstellen + $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel"; + $sql .= " WHERE fk_commande = ".((int)$order->id); + $sql .= " AND date_stundenzettel = '".$db->idate($date)."'"; + $sql .= " AND status = 0"; // Nur Entwürfe + + $resql = $db->query($sql); + if ($resql && $db->num_rows($resql) > 0) { + $obj = $db->fetch_object($resql); + $use_stundenzettel_id = $obj->rowid; + $stundenzettel->fetch($use_stundenzettel_id); + } else { + // Neuen erstellen + $stundenzettel->fk_commande = $order->id; + $stundenzettel->fk_soc = $order->socid; + $stundenzettel->date_stundenzettel = $date; + $use_stundenzettel_id = $stundenzettel->create($user); + } + } + + if ($use_stundenzettel_id > 0) { + // Produkte hinzufügen + foreach ($selected as $line_id) { + // Hole Zeile aus commandedet + $sql = "SELECT cd.rowid, cd.fk_product, cd.qty, cd.description,"; + $sql .= " p.ref as product_ref, p.label as product_label"; + $sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product"; + $sql .= " WHERE cd.rowid = ".((int)$line_id); + + $resql = $db->query($sql); + if ($resql && ($obj = $db->fetch_object($resql))) { + // Prüfe ob schon auf diesem Stundenzettel + $sql2 = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel_product"; + $sql2 .= " WHERE fk_stundenzettel = ".((int)$use_stundenzettel_id); + $sql2 .= " AND fk_commandedet = ".((int)$line_id); + $resql2 = $db->query($sql2); + if ($resql2 && $db->num_rows($resql2) == 0) { + // Noch nicht vorhanden, hinzufügen + $stundenzettel->addProduct( + $obj->fk_product, + $obj->rowid, + null, + $obj->qty, + 0, + 'order', + $obj->description // Beschreibung für Freitext-Produkte + ); + } + } + } + + // Mehraufwand-Produkte in Produktliste übernehmen (falls ausgewählt) + // Erstellt eine neue Produktzeile (origin='added') im Ziel-Stundenzettel + if (!empty($selected_mehraufwand)) { + foreach ($selected_mehraufwand as $ma_ids) { + // $ma_ids kann mehrere IDs enthalten (kommasepariert) + $ids = explode(',', $ma_ids); + foreach ($ids as $sp_id) { + $sp_id = (int)$sp_id; + if ($sp_id <= 0) continue; + + // Produktinfo aus der Mehraufwand-Zeile holen + $sqlMa = "SELECT sp.fk_product, sp.product_ref, sp.product_label, sp.description, sp.qty_done"; + $sqlMa .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; + $sqlMa .= " WHERE sp.rowid = ".((int)$sp_id); + $resqlMa = $db->query($sqlMa); + + if ($resqlMa && ($objMa = $db->fetch_object($resqlMa))) { + $qty = max(1, (float)$objMa->qty_done); // Mindestens 1 + + // Prüfe ob Produkt schon auf diesem Stundenzettel (in Produktliste) existiert + $sqlCheck = "SELECT rowid, qty_done FROM ".MAIN_DB_PREFIX."stundenzettel_product"; + $sqlCheck .= " WHERE fk_stundenzettel = ".((int)$use_stundenzettel_id); + $sqlCheck .= " AND origin = 'added'"; + if ($objMa->fk_product > 0) { + $sqlCheck .= " AND fk_product = ".((int)$objMa->fk_product); + } else { + $sqlCheck .= " AND fk_product IS NULL AND description = '".$db->escape($objMa->description)."'"; + } + $resqlCheck = $db->query($sqlCheck); + + if ($resqlCheck && $db->num_rows($resqlCheck) > 0) { + // Existiert bereits - Menge erhöhen + $objExist = $db->fetch_object($resqlCheck); + $newQty = (float)$objExist->qty_done + $qty; + $sqlUpd = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_product"; + $sqlUpd .= " SET qty_done = ".$newQty; + $sqlUpd .= " WHERE rowid = ".((int)$objExist->rowid); + $db->query($sqlUpd); + } else { + // Neu als Produktzeile anlegen (origin='added') + $stundenzettel->addProduct( + $objMa->fk_product, + null, // kein fk_commandedet (nicht im Auftrag) + null, // qty_original + 0, // qty_ordered + $qty, // qty_done + 'added', // origin = in Produktliste hinzugefügt + $objMa->description + ); + } + } + } + } + } + + setEventMessages($langs->trans('ProductsTransferred'), null, 'mesgs'); + header('Location: '.dol_buildpath('/stundenzettel/card.php?id='.$use_stundenzettel_id.'&tab=products', 1)); + exit; + } else { + setEventMessages($langs->trans('ErrorCreatingStundenzettel'), null, 'errors'); + } + } +} + +// Notiz abhaken (von stundenzettel_commande aus) +if ($action == 'toggle_note' && $user->hasRight('stundenzettel', 'write')) { + $note_id = GETPOST('note_id', 'int'); + $checked = GETPOST('checked', 'int'); + $stz_id = GETPOST('stundenzettel_id', 'int'); + + if ($stz_id > 0) { + $stzObj = new Stundenzettel($db); + if ($stzObj->fetch($stz_id) > 0) { + $result = $stzObj->updateNoteStatus($note_id, $checked); + if ($result > 0) { + setEventMessages($langs->trans('RecordModified'), null, 'mesgs'); + } + } + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1'.($stundenzettel_id > 0 ? '&stundenzettel_id='.$stundenzettel_id : '')); + exit; +} + +// Stundenzettel für Auftrag freigeben (direkt oder nach Bestätigung) +// Berechtigung: write ist ausreichend (validate ist optional) +$canRelease = $user->hasRight('stundenzettel', 'write') || $user->hasRight('stundenzettel', 'validate') || $user->admin; + +$doRelease = false; +if ($action == 'release_stundenzettel' && $canRelease) { + $doRelease = true; +} +if ($action == 'release_stundenzettel_confirmed' && GETPOST('confirm', 'alpha') == 'yes' && $canRelease) { + $doRelease = true; +} +// Auch confirm_release akzeptieren (Fallback) +if ($action == 'confirm_release' && GETPOST('confirm', 'alpha') == 'yes' && $canRelease) { + $doRelease = true; +} + +if ($doRelease) { + // Direkt in Datenbank updaten (zuverlässiger als insertExtraFields) + $sql = "UPDATE ".MAIN_DB_PREFIX."commande_extrafields SET stundenzettel_status = 1 WHERE fk_object = ".((int)$order->id); + $resql = $db->query($sql); + + if ($resql) { + // Falls noch kein Eintrag existiert, einen erstellen + if ($db->affected_rows($resql) == 0) { + $sql2 = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_status) VALUES (".((int)$order->id).", 1)"; + $db->query($sql2); + } + setEventMessages($langs->trans('StundenzettelReleased'), null, 'mesgs'); + } else { + setEventMessages($db->lasterror(), null, 'errors'); + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1&filter='.$filter); + exit; +} + +// Stundenzettel für Auftrag wiedereröffnen +$canReopen = $user->hasRight('stundenzettel', 'write') || $user->hasRight('stundenzettel', 'validate') || $user->admin; +if ($action == 'reopen_stundenzettel' && $canReopen) { + // Direkt in Datenbank updaten (zuverlässiger als insertExtraFields) + $sql = "UPDATE ".MAIN_DB_PREFIX."commande_extrafields SET stundenzettel_status = 0 WHERE fk_object = ".((int)$order->id); + $resql = $db->query($sql); + + if ($resql) { + // Falls noch kein Eintrag existiert, einen erstellen + if ($db->affected_rows($resql) == 0) { + $sql2 = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_status) VALUES (".((int)$order->id).", 0)"; + $db->query($sql2); + } + setEventMessages($langs->trans('StundenzettelReopened'), null, 'mesgs'); + } else { + setEventMessages($db->lasterror(), null, 'errors'); + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1&filter='.$filter); + exit; +} + +// Stundenzettel komplett zurücksetzen (von Status 2 auf 0) +$canReset = $user->hasRight('stundenzettel', 'write') || $user->admin; +if ($action == 'reset_stundenzettel' && $canReset) { + // Direkt in Datenbank updaten (zuverlässiger als insertExtraFields) + $sql = "UPDATE ".MAIN_DB_PREFIX."commande_extrafields SET stundenzettel_status = 0 WHERE fk_object = ".((int)$order->id); + $resql = $db->query($sql); + + if ($resql) { + // Falls noch kein Eintrag existiert, einen erstellen + if ($db->affected_rows($resql) == 0) { + $sql2 = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_status) VALUES (".((int)$order->id).", 0)"; + $db->query($sql2); + } + setEventMessages($langs->trans('StundenzettelReset'), null, 'mesgs'); + } else { + setEventMessages($db->lasterror(), null, 'errors'); + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1&filter='.$filter); + exit; +} + +// In Rechnung übernehmen +if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes' && $user->hasRight('facture', 'creer')) { + require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php'; + + $db->begin(); + $error = 0; + + // Neue Rechnung erstellen + $facture = new Facture($db); + $facture->socid = $order->socid; + $facture->type = Facture::TYPE_STANDARD; + $facture->date = dol_now(); + $facture->fk_project = $order->fk_project; + $facture->cond_reglement_id = $order->cond_reglement_id; + $facture->mode_reglement_id = $order->mode_reglement_id; + $facture->note_private = $langs->trans('CreatedFromStundenzettel').' - '.$order->ref; + $facture->linked_objects['commande'] = $order->id; + + $facture_id = $facture->create($user); + + if ($facture_id > 0) { + // Sammle alle Produkt-Mengen aus Stundenzetteln (gruppiert nach fk_commandedet) + $productQtys = array(); + $sqlQty = "SELECT sp.fk_commandedet, SUM(sp.qty_done) as total_qty"; + $sqlQty .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; + $sqlQty .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; + $sqlQty .= " WHERE s.fk_commande = ".((int)$order->id); + $sqlQty .= " AND sp.origin IN ('order', 'added')"; + $sqlQty .= " AND sp.fk_commandedet IS NOT NULL"; + $sqlQty .= " GROUP BY sp.fk_commandedet"; + + $resqlQty = $db->query($sqlQty); + if ($resqlQty) { + while ($objQty = $db->fetch_object($resqlQty)) { + $productQtys[$objQty->fk_commandedet] = floatval($objQty->total_qty); + } + } + + // Sammle Mehraufwand (zusätzliche Produkte mit origin = 'additional') + $additionalProducts = array(); + $sqlAdd = "SELECT sp.fk_product, sp.product_label, sp.description, SUM(sp.qty_done) as total_qty"; + $sqlAdd .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; + $sqlAdd .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; + $sqlAdd .= " WHERE s.fk_commande = ".((int)$order->id); + $sqlAdd .= " AND sp.origin = 'additional'"; + $sqlAdd .= " GROUP BY sp.fk_product, sp.product_label, sp.description"; + + $resqlAdd = $db->query($sqlAdd); + if ($resqlAdd) { + while ($objAdd = $db->fetch_object($resqlAdd)) { + if (floatval($objAdd->total_qty) > 0) { + $additionalProducts[] = $objAdd; + } + } + } + + // Lade Section-Hierarchie aus llx_facture_lines_manager + $sqlManager = "SELECT m.rowid, m.line_type, m.fk_commandedet, m.title, m.parent_section, m.line_order, m.show_subtotal,"; + $sqlManager .= " cd.fk_product, cd.qty as qty_ordered, cd.subprice, cd.tva_tx, cd.remise_percent,"; + $sqlManager .= " cd.description, cd.product_type, cd.special_code, cd.fk_unit,"; + $sqlManager .= " p.ref as product_ref, p.label as product_label"; + $sqlManager .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager m"; + $sqlManager .= " LEFT JOIN ".MAIN_DB_PREFIX."commandedet cd ON cd.rowid = m.fk_commandedet"; + $sqlManager .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = cd.fk_product"; + $sqlManager .= " WHERE m.fk_commande = ".((int)$order->id); + $sqlManager .= " AND m.document_type = 'order'"; + $sqlManager .= " ORDER BY m.line_order"; + + $resqlManager = $db->query($sqlManager); + + if ($resqlManager) { + // Sammle alle Zeilen nach Parent-Section gruppiert + $sections = array(); // section_id => section data + $sectionProducts = array(); // section_id => array of products + $orphanProducts = array(); // Products without section + + while ($obj = $db->fetch_object($resqlManager)) { + if ($obj->line_type == 'section') { + $sections[$obj->rowid] = $obj; + if (!isset($sectionProducts[$obj->rowid])) { + $sectionProducts[$obj->rowid] = array(); + } + } elseif ($obj->line_type == 'product') { + $parentId = $obj->parent_section ? $obj->parent_section : 0; + if ($parentId > 0) { + if (!isset($sectionProducts[$parentId])) { + $sectionProducts[$parentId] = array(); + } + $sectionProducts[$parentId][] = $obj; + } else { + $orphanProducts[] = $obj; + } + } + // subtotal wird automatisch nach Section hinzugefügt wenn show_subtotal=1 + } + + $rang = 0; + $invoiceManagerLines = array(); // Für llx_facture_lines_manager der Rechnung + + // Verarbeite jede Section + foreach ($sections as $sectionId => $section) { + $products = isset($sectionProducts[$sectionId]) ? $sectionProducts[$sectionId] : array(); + $sectionHasInvoicedProducts = false; + $sectionSubtotal = 0; + + // Prüfe ob Section Produkte mit qty > 0 hat + foreach ($products as $prod) { + $qtyToInvoice = isset($productQtys[$prod->fk_commandedet]) ? $productQtys[$prod->fk_commandedet] : 0; + if ($qtyToInvoice > 0) { + $sectionHasInvoicedProducts = true; + break; + } + } + + // Nur Sections mit tatsächlich verwendeten Produkten hinzufügen + if ($sectionHasInvoicedProducts) { + // Section-Titel hinzufügen (special_code = 100) + $result = $facture->addline( + $section->title, + 0, // subprice + 0, // qty + 0, // tva_tx + 0, 0, // localtax + 0, // fk_product + 0, // remise_percent + '', '', // dates + 0, 0, '', 'HT', 0, + 9, // product_type (9 = Title) + $rang++, + 100, // special_code = 100 für Section + 0, '', 0, 0 + ); + + if ($result > 0) { + $invoiceManagerLines[] = array( + 'type' => 'section', + 'fk_facturedet' => $result, + 'title' => $section->title, + 'parent' => null + ); + $currentSectionDetId = $result; + } + + // Produkte der Section hinzufügen + foreach ($products as $prod) { + $qtyToInvoice = isset($productQtys[$prod->fk_commandedet]) ? $productQtys[$prod->fk_commandedet] : 0; + + if ($qtyToInvoice > 0) { + $result = $facture->addline( + $prod->description, // 1: desc + $prod->subprice, // 2: pu_ht + $qtyToInvoice, // 3: qty + $prod->tva_tx, // 4: txtva + 0, // 5: txlocaltax1 + 0, // 6: txlocaltax2 + $prod->fk_product, // 7: fk_product + $prod->remise_percent, // 8: remise_percent + '', // 9: date_start + '', // 10: date_end + 0, // 11: fk_code_ventilation + 0, // 12: info_bits + 0, // 13: fk_remise_except + 'HT', // 14: price_base_type + 0, // 15: pu_ttc + $prod->product_type, // 16: type + $rang++, // 17: rang + 0, // 18: special_code + '', // 19: origin + 0, // 20: origin_id + 0, // 21: fk_parent_line + null, // 22: fk_fournprice + 0, // 23: pa_ht + '', // 24: label + array(), // 25: array_options + 100, // 26: situation_percent + 0, // 27: fk_prev_id + $prod->fk_unit // 28: fk_unit + ); + + if ($result > 0) { + $invoiceManagerLines[] = array( + 'type' => 'product', + 'fk_facturedet' => $result, + 'title' => null, + 'parent' => $currentSectionDetId + ); + $sectionSubtotal += $prod->subprice * $qtyToInvoice; + } elseif ($result < 0) { + $error++; + setEventMessages($facture->error, $facture->errors, 'errors'); + } + } + } + + // Zwischensumme hinzufügen wenn Section show_subtotal hat + if ($section->show_subtotal && $sectionSubtotal > 0) { + $subtotalLabel = 'Zwischensumme: '.$section->title; + $result = $facture->addline( + $subtotalLabel, + 0, // wird automatisch berechnet + 1, // qty + 0, // tva_tx + 0, 0, // localtax + 0, // fk_product + 0, // remise_percent + '', '', // dates + 0, 0, '', 'HT', 0, + 9, // product_type + $rang++, + 102, // special_code = 102 für Subtotal + 0, '', 0, 0 + ); + + if ($result > 0) { + $invoiceManagerLines[] = array( + 'type' => 'subtotal', + 'fk_facturedet' => $result, + 'title' => $subtotalLabel, + 'parent' => $currentSectionDetId + ); + } + } + } + } + + // Prüfe ob im Auftrag überhaupt Sections vorhanden sind + $orderHasSections = (count($sections) > 0); + + // Orphan-Produkte (ohne Section) hinzufügen + // Nur als eigene Section "Sonstige Produkte" wenn im Auftrag Sections vorhanden sind + $hasOrphansWithQty = false; + foreach ($orphanProducts as $prod) { + $qtyToInvoice = isset($productQtys[$prod->fk_commandedet]) ? $productQtys[$prod->fk_commandedet] : 0; + if ($qtyToInvoice > 0) { + $hasOrphansWithQty = true; + break; + } + } + + if ($hasOrphansWithQty) { + $sonstigeSectionResult = 0; + + // Nur Section erstellen wenn im Auftrag auch Sections vorhanden sind + if ($orderHasSections) { + $sonstigeSectionResult = $facture->addline( + $langs->trans('OtherProducts'), + 0, 0, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0, + 9, // product_type für Titel + $rang++, + 100 // special_code = 100 für Section + ); + + if ($sonstigeSectionResult > 0) { + $invoiceManagerLines[] = array( + 'type' => 'section', + 'fk_facturedet' => $sonstigeSectionResult, + 'title' => $langs->trans('OtherProducts'), + 'parent' => null + ); + } + } + + foreach ($orphanProducts as $prod) { + $qtyToInvoice = isset($productQtys[$prod->fk_commandedet]) ? $productQtys[$prod->fk_commandedet] : 0; + + if ($qtyToInvoice > 0) { + $result = $facture->addline( + $prod->description, // 1: desc + $prod->subprice, // 2: pu_ht + $qtyToInvoice, // 3: qty + $prod->tva_tx, // 4: txtva + 0, // 5: txlocaltax1 + 0, // 6: txlocaltax2 + $prod->fk_product, // 7: fk_product + $prod->remise_percent, // 8: remise_percent + '', // 9: date_start + '', // 10: date_end + 0, // 11: fk_code_ventilation + 0, // 12: info_bits + 0, // 13: fk_remise_except + 'HT', // 14: price_base_type + 0, // 15: pu_ttc + $prod->product_type, // 16: type + $rang++, // 17: rang + 0, // 18: special_code + '', // 19: origin + 0, // 20: origin_id + 0, // 21: fk_parent_line + null, // 22: fk_fournprice + 0, // 23: pa_ht + '', // 24: label + array(), // 25: array_options + 100, // 26: situation_percent + 0, // 27: fk_prev_id + $prod->fk_unit // 28: fk_unit + ); + + if ($result > 0 && $orderHasSections && $sonstigeSectionResult > 0) { + $invoiceManagerLines[] = array( + 'type' => 'product', + 'fk_facturedet' => $result, + 'title' => null, + 'parent' => $sonstigeSectionResult + ); + } elseif ($result < 0) { + $error++; + setEventMessages($facture->error, $facture->errors, 'errors'); + } + } + } + + // Zwischensumme für Sonstige Produkte + if ($sonstigeSectionResult > 0) { + $subtotalLabel = 'Zwischensumme: '.$langs->trans('OtherProducts'); + $subtotalResult = $facture->addline( + $subtotalLabel, + 0, 1, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0, + 9, $rang++, 102, 0, '', 0, 0 + ); + if ($subtotalResult > 0) { + $invoiceManagerLines[] = array( + 'type' => 'subtotal', + 'fk_facturedet' => $subtotalResult, + 'title' => $subtotalLabel, + 'parent' => $sonstigeSectionResult + ); + } + } + } + + // Mehraufwand hinzufügen (am Ende) + // Nur als Section wenn im Auftrag auch Sections vorhanden sind + if (count($additionalProducts) > 0) { + $mehraufwandSectionResult = 0; + + // Nur Section erstellen wenn im Auftrag auch Sections vorhanden sind + if ($orderHasSections) { + $mehraufwandSectionResult = $facture->addline( + $langs->trans('Mehraufwand'), + 0, 0, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0, + 9, // product_type für Titel + $rang++, + 100 // special_code = 100 für Section + ); + + if ($mehraufwandSectionResult > 0) { + $invoiceManagerLines[] = array( + 'type' => 'section', + 'fk_facturedet' => $mehraufwandSectionResult, + 'title' => $langs->trans('Mehraufwand'), + 'parent' => null + ); + } + } + + foreach ($additionalProducts as $addProd) { + $productResult = 0; + if ($addProd->fk_product > 0) { + // Katalog-Produkt + $product = new Product($db); + $product->fetch($addProd->fk_product); + + $productResult = $facture->addline( + $product->label, // 1: desc + $product->price, // 2: pu_ht + $addProd->total_qty, // 3: qty + $product->tva_tx, // 4: txtva + 0, // 5: txlocaltax1 + 0, // 6: txlocaltax2 + $addProd->fk_product, // 7: fk_product + 0, // 8: remise_percent + '', // 9: date_start + '', // 10: date_end + 0, // 11: fk_code_ventilation + 0, // 12: info_bits + 0, // 13: fk_remise_except + 'HT', // 14: price_base_type + 0, // 15: pu_ttc + $product->type, // 16: type + $rang++, // 17: rang + 0, // 18: special_code + '', // 19: origin + 0, // 20: origin_id + 0, // 21: fk_parent_line + null, // 22: fk_fournprice + 0, // 23: pa_ht + '', // 24: label + array(), // 25: array_options + 100, // 26: situation_percent + 0, // 27: fk_prev_id + $product->fk_unit // 28: fk_unit + ); + } else { + // Freitext + $productResult = $facture->addline( + $addProd->description ? $addProd->description : $addProd->product_label, + 0, // 2: pu_ht + $addProd->total_qty, // 3: qty + 0, // 4: txtva + 0, // 5: txlocaltax1 + 0, // 6: txlocaltax2 + 0, // 7: fk_product + 0, // 8: remise_percent + '', // 9: date_start + '', // 10: date_end + 0, // 11: fk_code_ventilation + 0, // 12: info_bits + 0, // 13: fk_remise_except + 'HT', // 14: price_base_type + 0, // 15: pu_ttc + 0, // 16: type + $rang++, // 17: rang + 0, // 18: special_code + '', // 19: origin + 0, // 20: origin_id + 0, // 21: fk_parent_line + null, // 22: fk_fournprice + 0, // 23: pa_ht + '', // 24: label + array(), // 25: array_options + 100, // 26: situation_percent + 0, // 27: fk_prev_id + 0 // 28: fk_unit + ); + } + + if ($productResult > 0 && $orderHasSections && $mehraufwandSectionResult > 0) { + $invoiceManagerLines[] = array( + 'type' => 'product', + 'fk_facturedet' => $productResult, + 'title' => null, + 'parent' => $mehraufwandSectionResult + ); + } elseif ($productResult < 0) { + $error++; + setEventMessages($facture->error, $facture->errors, 'errors'); + } + } + + // Zwischensumme für Mehraufwand + if ($mehraufwandSectionResult > 0) { + $subtotalLabel = 'Zwischensumme: '.$langs->trans('Mehraufwand'); + $subtotalResult = $facture->addline( + $subtotalLabel, + 0, 1, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0, + 9, $rang++, 102, 0, '', 0, 0 + ); + if ($subtotalResult > 0) { + $invoiceManagerLines[] = array( + 'type' => 'subtotal', + 'fk_facturedet' => $subtotalResult, + 'title' => $subtotalLabel, + 'parent' => $mehraufwandSectionResult + ); + } + } + } + + // llx_facture_lines_manager für Rechnung erstellen + if (!$error && count($invoiceManagerLines) > 0) { + $lineOrder = 1; + $sectionMap = array(); // old_facturedet_id => new_manager_rowid + + foreach ($invoiceManagerLines as $mLine) { + $sqlInsert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager"; + $sqlInsert .= " (fk_facture, document_type, line_type, fk_facturedet, title, parent_section, line_order, show_subtotal, date_creation)"; + $sqlInsert .= " VALUES ("; + $sqlInsert .= ((int)$facture_id).", 'invoice', "; + $sqlInsert .= "'".$db->escape($mLine['type'])."', "; + $sqlInsert .= ($mLine['fk_facturedet'] ? (int)$mLine['fk_facturedet'] : "NULL").", "; + $sqlInsert .= ($mLine['title'] ? "'".$db->escape($mLine['title'])."'" : "NULL").", "; + + // Parent-Section mapping + if ($mLine['parent'] && isset($sectionMap[$mLine['parent']])) { + $sqlInsert .= (int)$sectionMap[$mLine['parent']].", "; + } else { + $sqlInsert .= "NULL, "; + } + + $sqlInsert .= $lineOrder++.", "; + $sqlInsert .= ($mLine['type'] == 'section' ? "1" : "0").", "; + $sqlInsert .= "NOW())"; + + $db->query($sqlInsert); + + // Track section IDs for parent mapping + if ($mLine['type'] == 'section') { + $sectionMap[$mLine['fk_facturedet']] = $db->last_insert_id(MAIN_DB_PREFIX.'facture_lines_manager'); + } + } + } + } else { + $error++; + } + + if (!$error) { + // ============================================ + // Rang-Werte in facturedet korrigieren + // Synchronisiert rang mit line_order aus facture_lines_manager + // ============================================ + $sqlFixRang = "UPDATE ".MAIN_DB_PREFIX."facturedet fd + INNER JOIN ".MAIN_DB_PREFIX."facture_lines_manager flm ON flm.fk_facturedet = fd.rowid + SET fd.rang = flm.line_order + WHERE flm.fk_facture = ".((int)$facture_id); + $db->query($sqlFixRang); + + // Status auf "in Rechnung übertragen" setzen (direkt per SQL) + $sqlUpdate = "UPDATE ".MAIN_DB_PREFIX."commande_extrafields SET stundenzettel_status = 2 WHERE fk_object = ".((int)$order->id); + $resqlUpdate = $db->query($sqlUpdate); + if (!$resqlUpdate || $db->affected_rows($resqlUpdate) == 0) { + $sqlInsert = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_status) VALUES (".((int)$order->id).", 2)"; + $db->query($sqlInsert); + } + + $db->commit(); + setEventMessages($langs->trans('InvoiceCreated'), null, 'mesgs'); + header('Location: '.DOL_URL_ROOT.'/compta/facture/card.php?id='.$facture_id); + exit; + } else { + $db->rollback(); + } + } else { + $error++; + setEventMessages($facture->error, $facture->errors, 'errors'); + $db->rollback(); + } + + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1'); + exit; +} + +/* + * View + */ + +// Linkes Menü aktivieren +$_GET['mainmenu'] = 'stundenzettel'; + +// Stundenzettel laden wenn ID übergeben wurde +$stundenzettelObj = null; +if ($stundenzettel_id > 0) { + dol_include_once('/stundenzettel/lib/stundenzettel.lib.php'); + $stundenzettelObj = new Stundenzettel($db); + if ($stundenzettelObj->fetch($stundenzettel_id) <= 0) { + $stundenzettelObj = null; + $stundenzettel_id = 0; + } +} + +// Lade Extrafields für Stundenzettel-Status - direkt aus DB lesen für Zuverlässigkeit +$sqlStatus = "SELECT stundenzettel_status FROM ".MAIN_DB_PREFIX."commande_extrafields WHERE fk_object = ".((int)$order->id); +$resqlStatus = $db->query($sqlStatus); +$stundenzettelStatus = 0; +if ($resqlStatus && ($objStatus = $db->fetch_object($resqlStatus))) { + $stundenzettelStatus = (int)$objStatus->stundenzettel_status; +} +// 0 = Offen, 1 = Freigegeben, 2 = In Rechnung übertragen + +// Prüfe ob alle Stundenzettel validiert sind +$allValidated = false; +$hasStundenzettel = false; +$sqlCheck = "SELECT COUNT(*) as total, SUM(CASE WHEN status >= 1 THEN 1 ELSE 0 END) as validated"; +$sqlCheck .= " FROM ".MAIN_DB_PREFIX."stundenzettel WHERE fk_commande = ".((int)$order->id); +$resqlCheck = $db->query($sqlCheck); +if ($resqlCheck) { + $objCheck = $db->fetch_object($resqlCheck); + $hasStundenzettel = ($objCheck->total > 0); + $allValidated = ($objCheck->total > 0 && $objCheck->total == $objCheck->validated); +} + +// Berechne verbleibende Produkte +$remainingProducts = array(); +if ($hasStundenzettel) { + $sqlRemaining = "SELECT cd.rowid, cd.fk_product, cd.qty as qty_ordered, cd.description,"; + $sqlRemaining .= " p.ref as product_ref, p.label as product_label,"; + $sqlRemaining .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; + $sqlRemaining .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; + $sqlRemaining .= " WHERE sp.fk_commandedet = cd.rowid AND sp.origin IN ('order', 'added', 'omitted')), 0) as qty_documented"; + $sqlRemaining .= " FROM ".MAIN_DB_PREFIX."commandedet as cd"; + $sqlRemaining .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product"; + $sqlRemaining .= " WHERE cd.fk_commande = ".((int)$order->id); + $sqlRemaining .= " AND (cd.fk_product > 0 OR (cd.fk_product = 0 AND cd.description IS NOT NULL AND cd.description != ''))"; + $sqlRemaining .= " AND (cd.special_code IS NULL OR cd.special_code = 0)"; + $sqlRemaining .= " HAVING qty_ordered > qty_documented"; + + $resqlRemaining = $db->query($sqlRemaining); + if ($resqlRemaining) { + while ($objRem = $db->fetch_object($resqlRemaining)) { + $remainingProducts[] = $objRem; + } + } +} +$hasRemainingProducts = (count($remainingProducts) > 0); + +$title = $langs->trans("Stundenzettel").' - '.$order->ref; +llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-stundenzettel page-commande'); + +// Tabs - Stundenzettel-Tabs wenn aus Stundenzettel aufgerufen, sonst Auftrags-Tabs +// Aktiven Tab-Key basierend auf $tab bestimmen +$activeTabKey = 'productlist'; // Default +if ($tab == 'stundenzettel') { + $activeTabKey = 'stundenzettel_list'; +} elseif ($tab == 'tracking') { + $activeTabKey = 'tracking'; +} + +if ($stundenzettelObj) { + $head = stundenzettel_prepare_head($stundenzettelObj); + dol_fiche_head($head, $activeTabKey, $langs->trans("Stundenzettel"), -1, 'clock'); +} else { + $head = commande_prepare_head($order); + dol_fiche_head($head, 'stundenzettel', $langs->trans("CustomerOrder"), -1, 'order'); +} + +$form = new Form($db); + + + +// Bestätigung für Freigabe mit Warnung (verbleibende Produkte) +if ($action == 'confirm_release_warning') { + // Zeige Warnung mit verbleibenden Produkten + if ($hasRemainingProducts && count($remainingProducts) > 0) { + // Erstelle HTML für die Produktliste + $remainingHtml = '
'; + $remainingHtml .= ''.$langs->trans("RemainingProductsWarning").':
'; + $confirmMessage = $langs->trans('ConfirmReleaseWithRemaining').$remainingHtml.'

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