From a69e308a689036f8cf496a20ed1a7a8f5dc896d5 Mon Sep 17 00:00:00 2001 From: data Date: Mon, 23 Feb 2026 10:17:06 +0100 Subject: [PATCH] =?UTF-8?q?Version=201.7.0:=20R=C3=BCcknahme-Anzeige=20und?= =?UTF-8?q?=20Filter=20verbessert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rücknahmen mit rotem Badge (-X) bei Beauftragt und Verbaut - Rücknahmen werden von der Zielmenge abgezogen (nicht wieder verbauen) - Status zeigt "Erledigt" wenn alles zurückgenommen wurde - Mehraufwand-Bereich wird nach Filter (offen/erledigt) gefiltert - Tracking-Tab berücksichtigt Rücknahmen korrekt - Freitext-Rücknahmen werden über description gematcht - Bugfix: product_label bei Freitext-Rücknahmen korrekt gesetzt - Dokumentation und Berechtigungen aktualisiert Co-Authored-By: Claude Opus 4.5 --- README.md | 20 +- card.php | 14 +- class/stundenzettel.class.php | 8 +- core/modules/modStundenzettel.class.php | 2 +- langs/de_DE/stundenzettel.lang | 2 + stundenzettel_commande.php | 262 ++++++++++++++++++++---- 6 files changed, 257 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index a18052c..1b3d2ca 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Stundenzettel Modul für Dolibarr -**Version:** 1.6.0 +**Version:** 1.7.0 **Autor:** Data IT Solution **Kompatibilität:** Dolibarr 16.0+ **Lizenz:** GPL v3 @@ -97,10 +97,13 @@ Sie können beim Kunden (unter **Kunden > Kundenkarte**) eine Standard-Leistung | Berechtigung | Beschreibung | |--------------|--------------| -| Lesen | Stundenzettel anzeigen | -| Erstellen/Bearbeiten | Stundenzettel erstellen und bearbeiten | +| Eigene Stundenzettel lesen | Eigene Stundenzettel anzeigen | +| Alle Stundenzettel lesen | Alle Stundenzettel anzeigen | +| Stundenzettel erstellen | Stundenzettel erstellen | +| Alle Stundenzettel bearbeiten | Alle Stundenzettel bearbeiten (Admin) | | Freigeben | Stundenzettel freigeben/sperren | -| Löschen | Stundenzettel löschen | +| Eigene Stundenzettel löschen | Eigene Stundenzettel löschen | +| Alle Stundenzettel löschen | Alle Stundenzettel löschen (Admin) | ## Workflow @@ -114,6 +117,15 @@ Sie können beim Kunden (unter **Kunden > Kundenkarte**) eine Standard-Leistung ## Changelog +### Version 1.7.0 +- **Rücknahme-Anzeige verbessert**: Rücknahmen werden mit rotem Badge (-X) angezeigt +- **Rücknahme beeinflusst Beauftragt**: Zurückgenommene Mengen werden von der Zielmenge abgezogen (soll nicht wieder verbaut werden) +- **Status korrekt bei Rücknahme**: Wenn alles zurückgenommen wurde, zeigt der Status "Erledigt" statt "Offen" +- **Mehraufwand-Filter**: Der Mehraufwand-Bereich wird jetzt auch nach dem aktiven Filter (offen/erledigt/alle) gefiltert +- **Tracking-Tab Rücknahmen**: Rücknahmen werden auch im Tracking-Tab (Lieferauflistung) korrekt berücksichtigt +- **Freitext-Rücknahmen**: Rücknahmen von Freitext-Produkten werden über description gematcht und korrekt zugeordnet +- **Bugfix Freitext-Label**: Bei Rücknahme von Freitext-Produkten wird jetzt auch product_label korrekt gesetzt + ### Version 1.6.0 - **Stundenübernahme-Modus pro Auftrag**: Neues Extrafeld am Auftrag zur Wahl zwischen gruppierten Leistungen (Standard) und pro-Stundenzettel-Übernahme - **Leistungsbeschreibungen in Rechnung**: Arbeitsbeschreibungen aus den Stundenzetteln werden automatisch in die Rechnungszeilen übernommen diff --git a/card.php b/card.php index ed4df5c..93f4b1b 100644 --- a/card.php +++ b/card.php @@ -629,6 +629,7 @@ if ($action == 'add_ruecknahme' && $permissiontoadd) { // Prüfen ob es ein Freitext-Produkt oder normales Produkt ist $fk_product = 0; $freetext_description = ''; + $freetext_label = ''; // Label für Freitext-Produkte (wird in product_label gespeichert) $commandedet_id = 0; if (strpos($ruecknahme_product_raw, 'freetext_') === 0) { @@ -639,24 +640,24 @@ if ($action == 'add_ruecknahme' && $permissiontoadd) { $resqlDesc = $db->query($sqlDesc); if ($resqlDesc && ($objDesc = $db->fetch_object($resqlDesc))) { $freetext_description = $objDesc->description; + $freetext_label = strip_tags($objDesc->description); // Label = Freitext ohne HTML } } elseif (strpos($ruecknahme_product_raw, 'mehraufwand_') === 0) { // Freitext-Mehraufwand aus stundenzettel_product $sp_rowid = (int)substr($ruecknahme_product_raw, 12); - $sqlDesc = "SELECT description FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE rowid = ".((int)$sp_rowid); + $sqlDesc = "SELECT description, product_label FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE rowid = ".((int)$sp_rowid); $resqlDesc = $db->query($sqlDesc); if ($resqlDesc && ($objDesc = $db->fetch_object($resqlDesc))) { $freetext_description = $objDesc->description; + // Bei Freitext-Mehraufwand ist product_label leer, also description als Label nutzen + $freetext_label = !empty($objDesc->product_label) ? $objDesc->product_label : strip_tags($objDesc->description); } } else { $fk_product = (int)$ruecknahme_product_raw; } - // Beschreibung: Freitext-Beschreibung + Grund + // Beschreibung für die Rücknahme: nur der Grund (das Label steht separat) $description = $reason; - if (!empty($freetext_description)) { - $description = strip_tags($freetext_description) . (!empty($reason) ? ' - ' . $reason : ''); - } $error = 0; @@ -715,7 +716,8 @@ if ($action == 'add_ruecknahme' && $permissiontoadd) { 0, // qty_original $qty, // qty_done (Menge die zurückgenommen wird) 'returned', // origin (rücknahme) - $description // description (Grund) + $description, // description (Grund) + $freetext_label // product_label für Freitext-Produkte ); if ($result > 0) { setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); diff --git a/class/stundenzettel.class.php b/class/stundenzettel.class.php index 7162816..b84f65d 100644 --- a/class/stundenzettel.class.php +++ b/class/stundenzettel.class.php @@ -608,9 +608,10 @@ class Stundenzettel extends CommonObject * @param float $qty_done Done qty * @param string $origin Origin (order or added) * @param string $description Description (for free-text products) + * @param string $product_label_override Label für Freitext-Produkte (ohne fk_product) * @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 = '') + public function addProduct($fk_product, $fk_commandedet = null, $fk_manager_line = null, $qty_original = 0, $qty_done = 0, $origin = 'order', $description = '', $product_label_override = '') { global $db; @@ -626,6 +627,11 @@ class Stundenzettel extends CommonObject } } + // Override für Freitext-Produkte (kein fk_product, aber Label vorhanden) + if (!empty($product_label_override) && empty($product_label)) { + $product_label = $product_label_override; + } + // 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); diff --git a/core/modules/modStundenzettel.class.php b/core/modules/modStundenzettel.class.php index e68ed25..8021a1c 100644 --- a/core/modules/modStundenzettel.class.php +++ b/core/modules/modStundenzettel.class.php @@ -53,7 +53,7 @@ class modStundenzettel extends DolibarrModules $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.6.0'; + $this->version = '1.7.0'; // Autor $this->editor_name = 'Data IT Solution'; diff --git a/langs/de_DE/stundenzettel.lang b/langs/de_DE/stundenzettel.lang index 11df85d..6f08335 100644 --- a/langs/de_DE/stundenzettel.lang +++ b/langs/de_DE/stundenzettel.lang @@ -180,6 +180,8 @@ 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 +RuecknahmeOhneProdukt = Rücknahme ohne passendes Produkt gefunden +Error = Fehler # Widgets BoxRecentStundenzettel = Zuletzt bearbeitete Stundenzettel diff --git a/stundenzettel_commande.php b/stundenzettel_commande.php index 1d5e287..d4f45fd 100644 --- a/stundenzettel_commande.php +++ b/stundenzettel_commande.php @@ -1756,8 +1756,10 @@ if ($tab == 'products') { $qty_removed = isset($obj->qty_removed) ? (float)$obj->qty_removed : 0; $qty_returned = isset($obj->qty_returned) ? (float)$obj->qty_returned : 0; - // Effektive Gesamtmenge = Original + Hinzugefügt - Entfallen - $effectiveTotal = $obj->qty + $qty_added - $qty_removed; + // Effektive Gesamtmenge = Original + Hinzugefügt - Entfallen - Rücknahmen + // Rücknahmen werden auch von der Zielmenge abgezogen (soll nicht wieder verbaut werden) + $effectiveTotalBase = $obj->qty + $qty_added - $qty_removed; + $effectiveTotal = $effectiveTotalBase - $qty_returned; // Effektive Liefermenge = Geliefert - Zurückgenommen $effectiveDelivered = $obj->qty_delivered - $qty_returned; $remaining = $effectiveTotal - $effectiveDelivered; @@ -1818,13 +1820,16 @@ if ($tab == 'products') { if ($qty_removed > 0) { print ' -'.formatQty($qty_removed).''; } + if ($qty_returned > 0) { + print ' -'.formatQty($qty_returned).''; + } print ''; // Menge geliefert/verbaut (abzüglich Rücknahmen) print ''; print formatQty($effectiveDelivered); if ($qty_returned > 0) { - print ' -'.formatQty($qty_returned).''; + print ' -'.formatQty($qty_returned).''; } print ''; @@ -1959,6 +1964,7 @@ if ($tab == 'products') { // Logik: // - Wenn NUR 'added' existiert: Beauftragt = Verbaut = Menge // - Wenn 'additional' existiert: Beauftragt von 'additional', Verbaut von 'added' + // - Rücknahmen werden separat behandelt und müssen ein passendes Produkt finden $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,"; @@ -1968,29 +1974,82 @@ if ($tab == 'products') { $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 + // Nur Mehraufwand und hinzugefügt (Rücknahmen werden separat geladen) $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); + // Rücknahmen separat laden um sie den Produkten zuzuordnen + $sqlReturned = "SELECT sp.fk_product, sp.product_label, sp.description, SUM(sp.qty_done) as qty_returned,"; + $sqlReturned .= " GROUP_CONCAT(DISTINCT s.ref SEPARATOR ', ') as stundenzettel_refs"; + $sqlReturned .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; + $sqlReturned .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; + $sqlReturned .= " WHERE s.fk_commande = ".((int)$order->id); + $sqlReturned .= " AND sp.origin = 'returned'"; + $sqlReturned .= " GROUP BY sp.fk_product, sp.product_label, sp.description"; + + // Rücknahmen in Array laden für schnellen Zugriff + $returnedProducts = array(); + $unmatchedReturns = array(); // Rücknahmen ohne passendes Produkt + $resqlReturned = $db->query($sqlReturned); + if ($resqlReturned) { + while ($objRet = $db->fetch_object($resqlReturned)) { + $key = ''; + if ($objRet->fk_product > 0) { + $key = 'prod_'.$objRet->fk_product; + } else { + // Bei Freitext: Über description matchen + $key = 'desc_'.md5(trim(strip_tags($objRet->description))); + } + $returnedProducts[$key] = (float)$objRet->qty_returned; + } } - if ($mehraufwandCount > 0) { + $resqlMehraufwand = $db->query($sqlMehraufwand); + $mehraufwandRows = array(); + $mehraufwandVisibleCount = 0; + + // Erst alle Zeilen laden und filtern + if ($resqlMehraufwand) { + while ($objMa = $db->fetch_object($resqlMehraufwand)) { + // Berechne Status für Filter + $qtyAdditionalPre = (float)$objMa->qty_additional; + $qtyAddedPre = (float)$objMa->qty_added; + $returnKeyPre = ($objMa->fk_product > 0) ? 'prod_'.$objMa->fk_product : 'desc_'.md5(trim(strip_tags($objMa->description))); + $qtyReturnedPre = isset($returnedProducts[$returnKeyPre]) ? $returnedProducts[$returnKeyPre] : 0; + $qtyTargetPre = (($qtyAdditionalPre > 0) ? $qtyAdditionalPre : $qtyAddedPre) - $qtyReturnedPre; + $qtyDonePre = $qtyAddedPre - $qtyReturnedPre; + $qtyRemainingPre = $qtyTargetPre - $qtyDonePre; + $isDonePre = ($qtyRemainingPre <= 0) || ($qtyTargetPre <= 0 && $qtyReturnedPre > 0); + + // Filter anwenden + if ($filter == 'open' && $isDonePre) { + continue; + } + if ($filter == 'done' && !$isDonePre) { + continue; + } + + $mehraufwandRows[] = $objMa; + $mehraufwandVisibleCount++; + } + } + + if ($mehraufwandVisibleCount > 0) { // Mehraufwand-Header - einklappbar, dezente Farbgebung print ''; print ''; print ''; print ''; print $langs->trans("Mehraufwand"); - print ' ('.$mehraufwandCount.' '.$langs->trans("Products").')'; + print ' ('.$mehraufwandVisibleCount.' '.$langs->trans("Products").')'; print ''; print ''; - while ($objMa = $db->fetch_object($resqlMehraufwand)) { + // Verfolge welche Rücknahmen zugeordnet wurden + $matchedReturnKeys = array(); + + foreach ($mehraufwandRows as $objMa) { // 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) @@ -1999,10 +2058,25 @@ if ($tab == 'products') { $qtyAdditional = (float)$objMa->qty_additional; $qtyAdded = (float)$objMa->qty_added; + // Rücknahmen für dieses Produkt finden + $qtyReturned = 0; + $returnKey = ''; + if ($objMa->fk_product > 0) { + $returnKey = 'prod_'.$objMa->fk_product; + } else { + $returnKey = 'desc_'.md5(trim(strip_tags($objMa->description))); + } + if (isset($returnedProducts[$returnKey])) { + $qtyReturned = $returnedProducts[$returnKey]; + $matchedReturnKeys[$returnKey] = true; + } + // 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; + // MINUS Rücknahmen (was zurückgenommen wurde soll nicht wieder verbaut werden) + $qtyTargetBase = ($qtyAdditional > 0) ? $qtyAdditional : $qtyAdded; + $qtyTarget = $qtyTargetBase - $qtyReturned; + // Verbaut: Hinzugefügte Menge MINUS Rücknahmen + $qtyDone = $qtyAdded - $qtyReturned; $qtyRemaining = $qtyTarget - $qtyDone; // Verbleibend @@ -2023,7 +2097,8 @@ if ($tab == 'products') { $productIds = $objIds->product_ids; } - $isDone = ($qtyRemaining <= 0 && $qtyDone > 0); + // Status berechnen: Erledigt wenn alles verbaut ODER alles zurückgenommen + $isDone = ($qtyRemaining <= 0) || ($qtyTarget <= 0 && $qtyReturned > 0); print ''; @@ -2052,11 +2127,21 @@ if ($tab == 'products') { print ' '.$langs->trans("Mehraufwand").''; print ''; - // Menge beauftragt (Zielmenge - was verbaut werden soll) - print ''.formatQty($qtyTarget).''; + // Menge beauftragt (Zielmenge - was verbaut werden soll, MINUS Rücknahmen) + print ''; + print formatQty($qtyTarget); + if ($qtyReturned > 0) { + print ' -'.formatQty($qtyReturned).''; + } + print ''; - // Menge tatsächlich verbaut - print ''.formatQty($qtyDone).''; + // Menge tatsächlich verbaut (mit Rücknahme-Hinweis wenn vorhanden) + print ''; + print formatQty($qtyDone); + if ($qtyReturned > 0) { + print ' -'.formatQty($qtyReturned).''; + } + print ''; // Verbleibend (Zielmenge - Verbaut) print ''; @@ -2072,9 +2157,12 @@ if ($tab == 'products') { // Status print ''; - if ($qtyDone > 0 && $qtyRemaining > 0) { + if ($qtyTarget <= 0 && $qtyReturned > 0) { + // Alles zurückgenommen - Erledigt (mit Rücknahme-Hinweis) + print ''.$langs->trans("TrackingDone").''; + } elseif ($qtyDone > 0 && $qtyRemaining > 0) { print ''.$langs->trans("TrackingPartial").''; - } elseif ($qtyDone == 0) { + } elseif ($qtyDone == 0 && $qtyTarget > 0) { print ''.$langs->trans("TrackingOpen").''; } else { print ''.$langs->trans("TrackingDone").''; @@ -2084,6 +2172,44 @@ if ($tab == 'products') { print ''; } + + // Nicht zugeordnete Rücknahmen anzeigen (Warnung) + foreach ($returnedProducts as $retKey => $retQty) { + if (!isset($matchedReturnKeys[$retKey])) { + // Diese Rücknahme hat kein passendes Mehraufwand-Produkt gefunden + // Lade die Details aus der DB + $sqlUnmatched = "SELECT sp.product_label, sp.description, GROUP_CONCAT(DISTINCT s.ref SEPARATOR ', ') as stz_refs"; + $sqlUnmatched .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; + $sqlUnmatched .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; + $sqlUnmatched .= " WHERE s.fk_commande = ".((int)$order->id); + $sqlUnmatched .= " AND sp.origin = 'returned'"; + if (strpos($retKey, 'prod_') === 0) { + $sqlUnmatched .= " AND sp.fk_product = ".((int)substr($retKey, 5)); + } else { + // Freitext - über MD5 der description bereits gefiltert, hole alle + $sqlUnmatched .= " AND sp.fk_product IS NULL"; + } + $sqlUnmatched .= " GROUP BY sp.product_label, sp.description"; + $resqlUnmatched = $db->query($sqlUnmatched); + + if ($resqlUnmatched && ($objUnm = $db->fetch_object($resqlUnmatched))) { + print ''; + print ''; + print ''; + print ''; + $desc = !empty($objUnm->product_label) ? $objUnm->product_label : strip_tags($objUnm->description); + print ''.$desc.''; + print ' '.$langs->trans("Ruecknahme").''; + print ''; + print '-'; + print '-'.formatQty($retQty).''; + print '!'; + print ''.$langs->trans("Error").''; + print ' ('.$objUnm->stz_refs.')'; + print ''; + } + } + } } // HINWEIS: Entfällt-Produkte werden nicht separat angezeigt @@ -2312,7 +2438,12 @@ if ($tab == 'tracking') { $sql .= " COALESCE((SELECT SUM(sp3.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp3"; $sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s3 ON s3.rowid = sp3.fk_stundenzettel"; $sql .= " WHERE sp3.origin = 'omitted' AND s3.fk_commande = ".((int)$order->id); - $sql .= " AND ((sp3.fk_product = cd.fk_product AND cd.fk_product > 0) OR sp3.fk_commandedet = cd.rowid)), 0) as qty_omitted"; + $sql .= " AND ((sp3.fk_product = cd.fk_product AND cd.fk_product > 0) OR sp3.fk_commandedet = cd.rowid)), 0) as qty_omitted,"; + // Rücknahmen - für Produkte via fk_product, für Freitext via fk_commandedet + $sql .= " COALESCE((SELECT SUM(sp4.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp4"; + $sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s4 ON s4.rowid = sp4.fk_stundenzettel"; + $sql .= " WHERE sp4.origin = 'returned' AND s4.fk_commande = ".((int)$order->id); + $sql .= " AND ((sp4.fk_product = cd.fk_product AND cd.fk_product > 0) OR sp4.fk_commandedet = cd.rowid)), 0) as qty_returned"; $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); @@ -2329,8 +2460,12 @@ if ($tab == 'tracking') { $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; + $qty_returned = isset($obj->qty_returned) ? (float)$obj->qty_returned : 0; + // Effektiv bestellt = Original + Mehraufwand - Entfällt - Rücknahmen + $effective_ordered = $obj->qty_ordered + $qty_additional - $qty_omitted - $qty_returned; + // Effektiv geliefert = Geliefert - Rücknahmen + $effective_delivered = $obj->qty_delivered - $qty_returned; + $qty_remaining = $effective_ordered - $effective_delivered; // Details für dieses Produkt $details = isset($trackingDetails[$obj->rowid]) ? $trackingDetails[$obj->rowid] : array(); @@ -2365,7 +2500,7 @@ if ($tab == 'tracking') { } print ''; - // Bestellt (mit Badges für Mehraufwand/Entfällt) + // Bestellt (mit Badges für Mehraufwand/Entfällt/Rücknahmen) print ''; print ''.formatQty($effective_ordered).''; if ($qty_additional > 0) { @@ -2374,12 +2509,20 @@ if ($tab == 'tracking') { if ($qty_omitted > 0) { print ' -'.formatQty($qty_omitted).''; } + if ($qty_returned > 0) { + print ' -'.formatQty($qty_returned).''; + } print ''; $total_ordered += $effective_ordered; - // Geliefert/Erfasst - print ''.formatQty($obj->qty_delivered).''; - $total_delivered += $obj->qty_delivered; + // Geliefert/Erfasst (mit Rücknahme-Hinweis) + print ''; + print formatQty($effective_delivered); + if ($qty_returned > 0) { + print ' -'.formatQty($qty_returned).''; + } + print ''; + $total_delivered += $effective_delivered; // Verbleibend print ''; @@ -2395,9 +2538,12 @@ if ($tab == 'tracking') { // Status print ''; - if ($qty_remaining <= 0) { + if ($effective_ordered <= 0 && $qty_returned > 0) { + // Alles zurückgenommen - Erledigt print ''.$langs->trans("TrackingDone").''; - } elseif ($obj->qty_delivered > 0) { + } elseif ($qty_remaining <= 0) { + print ''.$langs->trans("TrackingDone").''; + } elseif ($effective_delivered > 0) { print ''.$langs->trans("TrackingPartial").''; } else { print ''.$langs->trans("TrackingOpen").''; @@ -2503,7 +2649,7 @@ if ($tab == 'tracking') { } // Mehraufwand und zusätzlich verbaute Produkte laden die NICHT im Auftrag sind - // Beauftragt (additional) und verbaut (added ohne fk_commandedet) getrennt summieren + // Beauftragt (additional) und verbaut (added ohne fk_commandedet) summieren $sqlMehraufwand = "SELECT sp.fk_product, sp.description,"; $sqlMehraufwand .= " p.ref as product_ref, p.label as product_label,"; $sqlMehraufwand .= " SUM(CASE WHEN sp.origin = 'additional' THEN sp.qty_done ELSE 0 END) as qty_additional,"; @@ -2519,6 +2665,21 @@ if ($tab == 'tracking') { $sqlMehraufwand .= " GROUP BY sp.fk_product, sp.description, p.ref, p.label"; $sqlMehraufwand .= " ORDER BY p.ref, sp.description"; + // Rücknahmen separat laden für Zuordnung (gleiche Logik wie in Produktliste) + $returnedMehr = array(); + $sqlRetMehr = "SELECT sp.fk_product, sp.description, SUM(sp.qty_done) as qty_returned"; + $sqlRetMehr .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; + $sqlRetMehr .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; + $sqlRetMehr .= " WHERE s.fk_commande = ".((int)$order->id)." AND sp.origin = 'returned'"; + $sqlRetMehr .= " GROUP BY sp.fk_product, sp.description"; + $resRetMehr = $db->query($sqlRetMehr); + if ($resRetMehr) { + while ($objRetM = $db->fetch_object($resRetMehr)) { + $keyM = ($objRetM->fk_product > 0) ? 'prod_'.$objRetM->fk_product : 'desc_'.md5(trim(strip_tags($objRetM->description))); + $returnedMehr[$keyM] = (float)$objRetM->qty_returned; + } + } + $resqlMehr = $db->query($sqlMehraufwand); $hasMehraufwandProducts = false; @@ -2533,10 +2694,20 @@ if ($tab == 'tracking') { $detailRowId++; $qtyAdditional = (float)$objMehr->qty_additional; $qtyAdded = (float)$objMehr->qty_added; + + // Rücknahmen für dieses Produkt finden + $qtyReturned = 0; + $retKeyM = ($objMehr->fk_product > 0) ? 'prod_'.$objMehr->fk_product : 'desc_'.md5(trim(strip_tags($objMehr->description))); + if (isset($returnedMehr[$retKeyM])) { + $qtyReturned = $returnedMehr[$retKeyM]; + } + // Beauftragt: Wenn Mehraufwand existiert, dessen Menge, sonst die verbaute Menge - $qty_ordered_mehr = ($qtyAdditional > 0) ? $qtyAdditional : $qtyAdded; - // Verbaut: Immer die tatsächlich verbaute Menge - $qty_delivered_mehr = $qtyAdded; + // MINUS Rücknahmen (was zurückgenommen wurde soll nicht wieder verbaut werden) + $qty_ordered_base = ($qtyAdditional > 0) ? $qtyAdditional : $qtyAdded; + $qty_ordered_mehr = $qty_ordered_base - $qtyReturned; + // Verbaut: Tatsächlich verbaute Menge MINUS Rücknahmen + $qty_delivered_mehr = $qtyAdded - $qtyReturned; $qty_remaining_mehr = $qty_ordered_mehr - $qty_delivered_mehr; // Details für Mehraufwand laden @@ -2579,12 +2750,22 @@ if ($tab == 'tracking') { } print ''; - // Bestellt (Mehraufwand-Menge) - print ''.formatQty($qty_ordered_mehr).''; + // Bestellt (Mehraufwand-Menge, MINUS Rücknahmen) + print ''; + print ''.formatQty($qty_ordered_mehr).''; + if ($qtyReturned > 0) { + print ' -'.formatQty($qtyReturned).''; + } + print ''; $total_ordered += $qty_ordered_mehr; - // Geliefert - print ''.formatQty($qty_delivered_mehr).''; + // Geliefert (mit Rücknahme-Hinweis wenn vorhanden) + print ''; + print formatQty($qty_delivered_mehr); + if ($qtyReturned > 0) { + print ' -'.formatQty($qtyReturned).''; + } + print ''; $total_delivered += $qty_delivered_mehr; // Verbleibend @@ -2601,7 +2782,10 @@ if ($tab == 'tracking') { // Status print ''; - if ($qty_remaining_mehr <= 0) { + if ($qty_ordered_mehr <= 0 && $qtyReturned > 0) { + // Alles zurückgenommen - Erledigt + print ''.$langs->trans("TrackingDone").''; + } elseif ($qty_remaining_mehr <= 0) { print ''.$langs->trans("TrackingDone").''; } elseif ($qty_delivered_mehr > 0) { print ''.$langs->trans("TrackingPartial").'';