* * 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; } /** * Prepare array of tabs for Stundenzettel Commande page (order-level view) * Diese Tabs werden immer angezeigt, unabhängig davon ob ein Stundenzettel ausgewählt ist * * @param Commande $order Das Auftrags-Objekt * @param int $stundenzettel_id Optional: ID des ausgewählten Stundenzettels * @return array Array of tabs */ function stundenzettel_commande_prepare_head($order, $stundenzettel_id = 0) { global $db, $langs, $conf, $user; $langs->load("stundenzettel@stundenzettel"); $h = 0; $head = array(); // Tab 1: Kundenauftrag (Link zurück zum Auftrag) $head[$h][0] = DOL_URL_ROOT.'/commande/card.php?id='.$order->id; $head[$h][1] = $langs->trans("Order"); $head[$h][2] = 'order'; $h++; // Tab 2: Produktliste $head[$h][0] = dol_buildpath('/stundenzettel/stundenzettel_commande.php', 1).'?id='.$order->id.'&tab=products&noredirect=1'.($stundenzettel_id > 0 ? '&stundenzettel_id='.$stundenzettel_id : ''); $head[$h][1] = $langs->trans("ProductList"); $head[$h][2] = 'products'; $h++; // Tab 3: Stundenzettel (aktueller/ausgewählter Stundenzettel - card.php) // Wenn kein Stundenzettel ausgewählt, den letzten offenen für diesen Auftrag suchen $activeStundenzettelId = $stundenzettel_id; if ($activeStundenzettelId <= 0) { $sqlActive = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel"; $sqlActive .= " WHERE fk_commande = ".((int)$order->id); $sqlActive .= " AND status = 0"; // Nur Entwürfe $sqlActive .= " ORDER BY date_stundenzettel DESC, rowid DESC"; $sqlActive .= " LIMIT 1"; $resqlActive = $db->query($sqlActive); if ($resqlActive && $db->num_rows($resqlActive) > 0) { $objActive = $db->fetch_object($resqlActive); $activeStundenzettelId = $objActive->rowid; } } if ($activeStundenzettelId > 0) { // Lade Stundenzettel für Badge-Berechnung require_once dol_buildpath('/stundenzettel/class/stundenzettel.class.php', 0); $tmpStz = new Stundenzettel($db); $tmpStz->fetch($activeStundenzettelId); $tmpStz->fetchLeistungen(); $tmpStz->fetchProducts(); $nbItems = count($tmpStz->leistungen) + count($tmpStz->products); $head[$h][0] = dol_buildpath('/stundenzettel/card.php', 1).'?id='.$activeStundenzettelId; $head[$h][1] = $langs->trans("Stundenzettel"); if ($nbItems > 0) { $head[$h][1] .= ''.$nbItems.''; } $head[$h][2] = 'card'; $h++; } // Tab 4: Alle Stundenzettel (Liste aller Stundenzettel für diesen Auftrag) $sql = "SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."stundenzettel WHERE fk_commande = ".((int)$order->id); $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='.$order->id.'&tab=stundenzettel&noredirect=1'.($stundenzettel_id > 0 ? '&stundenzettel_id='.$stundenzettel_id : ''); $head[$h][1] = $langs->trans("StundenzettelList"); if ($nbStundenzettel > 0) { $head[$h][1] .= ''.$nbStundenzettel.''; } $head[$h][2] = 'stundenzettel'; $h++; // Tab 4: Lieferauflistung (Tracking) $head[$h][0] = dol_buildpath('/stundenzettel/stundenzettel_commande.php', 1).'?id='.$order->id.'&tab=tracking&noredirect=1'.($stundenzettel_id > 0 ? '&stundenzettel_id='.$stundenzettel_id : ''); $head[$h][1] = $langs->trans("DeliveryTracking"); $head[$h][2] = 'tracking'; $h++; return $head; } /** * Holt den kundenspezifischen Preis für ein Produkt * Falls kein kundenspezifischer Preis existiert, wird der Standardpreis zurückgegeben * * @param DoliDB $db Datenbankverbindung * @param int $fk_product Produkt-ID * @param int $fk_soc Kunden-ID (Societe) * @param Product|null $product Optional: bereits geladenes Produkt-Objekt * @return array Array mit 'price' (HT), 'price_ttc', 'tva_tx', 'price_base_type', 'is_customer_price' */ function getCustomerPrice($db, $fk_product, $fk_soc, $product = null) { global $conf; $now = dol_now(); // Suche kundenspezifischen Preis in der Tabelle product_customer_price $sql = "SELECT price, price_ttc, tva_tx, price_base_type"; $sql .= " FROM ".MAIN_DB_PREFIX."product_customer_price"; $sql .= " WHERE fk_product = ".((int)$fk_product); $sql .= " AND fk_soc = ".((int)$fk_soc); $sql .= " AND entity IN (".getEntity('productprice').")"; // Prüfe Gültigkeitszeitraum (date_begin <= now und (date_end IS NULL oder date_end >= now)) $sql .= " AND date_begin <= '".$db->idate($now)."'"; $sql .= " AND (date_end IS NULL OR date_end >= '".$db->idate($now)."')"; $sql .= " ORDER BY date_begin DESC"; $sql .= " LIMIT 1"; $resql = $db->query($sql); if ($resql && $db->num_rows($resql) > 0) { $obj = $db->fetch_object($resql); return array( 'price' => (float)$obj->price, 'price_ttc' => (float)$obj->price_ttc, 'tva_tx' => (float)$obj->tva_tx, 'price_base_type' => $obj->price_base_type, 'is_customer_price' => true ); } // Kein kundenspezifischer Preis gefunden - lade Standardpreis vom Produkt if ($product === null) { require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; $product = new Product($db); $product->fetch($fk_product); } return array( 'price' => (float)$product->price, 'price_ttc' => (float)$product->price_ttc, 'tva_tx' => (float)$product->tva_tx, 'price_base_type' => $product->price_base_type, 'is_customer_price' => false ); } /** * Holt den effektiven Stundenpreis für einen Stundenzettel * Berücksichtigt: 1. Manuell gesetzter Preis im Stundenzettel * 2. Kundenspezifischer Preis * 3. Standard-Produktpreis * * @param DoliDB $db Datenbankverbindung * @param Stundenzettel $stundenzettel Das Stundenzettel-Objekt * @param int $defaultServiceId ID der Standard-Leistung * @return array Array mit 'price', 'source' ('custom', 'customer', 'standard') */ function getEffectiveHourlyRate($db, $stundenzettel, $defaultServiceId) { // 1. Prüfe ob manueller Preis im Stundenzettel gesetzt if ($stundenzettel->hourly_rate_is_custom && $stundenzettel->hourly_rate !== null) { return array( 'price' => (float)$stundenzettel->hourly_rate, 'source' => 'custom' ); } // 2. Hole kundenspezifischen oder Standard-Preis if ($defaultServiceId > 0) { $priceInfo = getCustomerPrice($db, $defaultServiceId, $stundenzettel->fk_soc); return array( 'price' => $priceInfo['price'], 'source' => $priceInfo['is_customer_price'] ? 'customer' : 'standard' ); } // 3. Kein Preis verfügbar return array( 'price' => 0, 'source' => 'none' ); } /** * Berechnet den Netto-Wert aller Stundenzettel eines Auftrags * und aktualisiert das Extrafield stundenzettel_netto * * @param DoliDB $db Datenbankverbindung * @param int $fk_commande Auftrags-ID * @return float Der berechnete Netto-Wert */ function updateOrderNettoSTZ($db, $fk_commande) { global $conf; if (empty($fk_commande)) { dol_syslog("updateOrderNettoSTZ: fk_commande is empty", LOG_WARNING); return 0; } $totalNetto = 0; // Hole Kunden-ID und Standard-Leistung ZUERST (vor der Stundenzettel-Schleife) $defaultServiceId = 0; $socid = 0; // Hole Kunden-ID vom Auftrag $sqlOrder = "SELECT fk_soc FROM ".MAIN_DB_PREFIX."commande WHERE rowid = ".((int)$fk_commande); $resqlOrder = $db->query($sqlOrder); if ($resqlOrder && ($objOrder = $db->fetch_object($resqlOrder))) { $socid = (int)$objOrder->fk_soc; // Hole Standard-Leistung vom Kunden (Extrafield) $sqlService = "SELECT stundenzettel_default_service FROM ".MAIN_DB_PREFIX."societe_extrafields"; $sqlService .= " WHERE fk_object = ".((int)$socid); $resqlService = $db->query($sqlService); if ($resqlService && ($objService = $db->fetch_object($resqlService))) { $defaultServiceId = (int)$objService->stundenzettel_default_service; } } dol_syslog("updateOrderNettoSTZ: commande=".$fk_commande.", socid=".$socid.", defaultServiceId=".$defaultServiceId, LOG_DEBUG); // 1. Alle freigegebenen Stundenzettel des Auftrags laden (status >= 1 = validiert) $sqlStz = "SELECT s.rowid, s.fk_soc, s.hourly_rate, s.hourly_rate_is_custom"; $sqlStz .= " FROM ".MAIN_DB_PREFIX."stundenzettel as s"; $sqlStz .= " WHERE s.fk_commande = ".((int)$fk_commande); $sqlStz .= " AND s.status >= 1"; // Nur validierte/freigegebene Stundenzettel $resqlStz = $db->query($sqlStz); if (!$resqlStz) { dol_syslog("updateOrderNettoSTZ: SQL error: ".$db->lasterror(), LOG_ERR); return 0; } $numStz = $db->num_rows($resqlStz); dol_syslog("updateOrderNettoSTZ: Found ".$numStz." validated Stundenzettel", LOG_DEBUG); while ($stz = $db->fetch_object($resqlStz)) { // 2. Produkte dieses Stundenzettels summieren $sqlProd = "SELECT sp.fk_product, sp.fk_commandedet, sp.qty_done, sp.origin,"; $sqlProd .= " cd.subprice as order_price, cd.tva_tx"; $sqlProd .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product as sp"; $sqlProd .= " LEFT JOIN ".MAIN_DB_PREFIX."commandedet as cd ON cd.rowid = sp.fk_commandedet"; $sqlProd .= " WHERE sp.fk_stundenzettel = ".((int)$stz->rowid); $sqlProd .= " AND sp.origin IN ('order', 'added', 'extra')"; // Nicht 'omitted' (entfällt) $resqlProd = $db->query($sqlProd); if ($resqlProd) { while ($prod = $db->fetch_object($resqlProd)) { $price = 0; $qty = (float)$prod->qty_done; if ($prod->fk_commandedet > 0 && $prod->order_price > 0) { // Preis aus Auftragszeile $price = (float)$prod->order_price; } elseif ($prod->fk_product > 0) { // Kundenspezifischer oder Standard-Preis $priceInfo = getCustomerPrice($db, $prod->fk_product, $socid); $price = $priceInfo['price']; } $totalNetto += $price * $qty; } } // 3. Leistungen (Arbeitsstunden) - JEDE Zeile einzeln mit ihrer gewählten Leistungsposition $sqlHours = "SELECT l.duration, l.fk_product FROM ".MAIN_DB_PREFIX."stundenzettel_leistung l"; $sqlHours .= " WHERE l.fk_stundenzettel = ".((int)$stz->rowid); $resqlHours = $db->query($sqlHours); if ($resqlHours) { while ($leistung = $db->fetch_object($resqlHours)) { $minutes = (float)$leistung->duration; $hoursWorked = $minutes / 60; if ($hoursWorked > 0) { $hourlyRate = 0; // Priorität: 1. Leistungsposition der Zeile, 2. Stundenzettel-Rate, 3. Kunden-Standard if ($leistung->fk_product > 0) { // Preis der gewählten Leistungsposition (mit Kundenpreis falls vorhanden) $priceInfo = getCustomerPrice($db, $leistung->fk_product, $socid); $hourlyRate = $priceInfo['price']; dol_syslog("updateOrderNettoSTZ: Leistung fk_product=".$leistung->fk_product." price=".$hourlyRate, LOG_DEBUG); } elseif ($stz->hourly_rate > 0) { // Fallback: Manueller Preis im Stundenzettel $hourlyRate = (float)$stz->hourly_rate; dol_syslog("updateOrderNettoSTZ: Using STZ hourly_rate=".$hourlyRate, LOG_DEBUG); } elseif ($defaultServiceId > 0) { // Fallback: Standard-Leistung des Kunden $priceInfo = getCustomerPrice($db, $defaultServiceId, $socid); $hourlyRate = $priceInfo['price']; dol_syslog("updateOrderNettoSTZ: Using defaultService price=".$hourlyRate, LOG_DEBUG); } else { dol_syslog("updateOrderNettoSTZ: No price for leistung! fk_product=".$leistung->fk_product, LOG_WARNING); } $subtotal = $hourlyRate * $hoursWorked; $totalNetto += $subtotal; dol_syslog("updateOrderNettoSTZ: subtotal=".$subtotal." (rate=".$hourlyRate." x hours=".$hoursWorked.")", LOG_DEBUG); } } } } dol_syslog("updateOrderNettoSTZ: TOTAL=".$totalNetto, LOG_DEBUG); // 4. Extrafield aktualisieren $sqlUpdate = "UPDATE ".MAIN_DB_PREFIX."commande_extrafields"; $sqlUpdate .= " SET stundenzettel_netto = ".((float)$totalNetto); $sqlUpdate .= " WHERE fk_object = ".((int)$fk_commande); $resqlUpdate = $db->query($sqlUpdate); if (!$resqlUpdate || $db->affected_rows($resqlUpdate) == 0) { // Zeile existiert noch nicht - INSERT $sqlInsert = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_netto)"; $sqlInsert .= " VALUES (".((int)$fk_commande).", ".((float)$totalNetto).")"; $db->query($sqlInsert); } return $totalNetto; }