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 '';
- 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").'';
|