* * 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. */ /** * \file class/preisbot.class.php * \ingroup preisbot * \brief PreisBot class for automatic price updates */ class PreisBot { /** * @var DoliDB Database handler */ public $db; /** * @var string Error message */ public $error = ''; /** * @var array Error messages */ public $errors = array(); /** * @var string Output for cronjob */ public $output = ''; /** * Constructor * * @param DoliDB $db Database handler */ public function __construct($db) { $this->db = $db; } /** * Cronjob method: Update product prices based on margin extrafield * * @return int 0 if OK, -1 if error */ public function updatePrices() { global $conf, $user, $langs; // Sicherheits-Timeout: Max 5 Minuten für den gesamten Job @set_time_limit(300); try { require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; // User-Kontext sicherstellen (Cronjob hat manchmal keinen User) if (empty($user) || !is_object($user) || empty($user->id)) { require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; $user = new User($this->db); $user->fetch(1); // Admin-User } $priceSource = getDolGlobalString('PREISBOT_PRICE_SOURCE', 'cheapest'); $priceDirection = getDolGlobalString('PREISBOT_PRICE_DIRECTION', 'up_only'); $minMargin = (float) getDolGlobalString('PREISBOT_MIN_MARGIN', 20); $updated = 0; $skipped = 0; $errors = 0; $priceChanges = array(); // Hole alle Produkte mit gesetztem Gewinnaufschlag (nur zum Verkauf stehende) $sql = "SELECT p.rowid, p.ref, p.label, p.price, pe.preisbot_margin"; $sql .= " FROM ".$this->db->prefix()."product as p"; $sql .= " LEFT JOIN ".$this->db->prefix()."product_extrafields as pe ON pe.fk_object = p.rowid"; $sql .= " WHERE pe.preisbot_margin IS NOT NULL"; $sql .= " AND pe.preisbot_margin >= ".((float) $minMargin); $sql .= " AND p.tosell = 1"; $sql .= " AND p.entity IN (".getEntity('product').")"; $resql = $this->db->query($sql); if (!$resql) { $this->error = $this->db->lasterror(); return -1; } while ($obj = $this->db->fetch_object($resql)) { $productId = $obj->rowid; $currentPrice = (float) $obj->price; $margin = (float) $obj->preisbot_margin; // Hole Einkaufspreis basierend auf Einstellung $buyPrice = $this->getSupplierPrice($productId, $priceSource); if ($buyPrice <= 0) { $skipped++; continue; } // Berechne neuen Verkaufspreis $newPrice = $buyPrice * (1 + ($margin / 100)); $newPrice = round($newPrice, 2); // Prüfe Preisrichtung if ($priceDirection === 'up_only' && $newPrice < $currentPrice) { $skipped++; continue; } // Nur aktualisieren wenn Preis sich ändert if (abs($newPrice - $currentPrice) < 0.01) { $skipped++; continue; } // Preis aktualisieren $product = new Product($this->db); $product->fetch($productId); $result = $product->updatePrice( $newPrice, $product->price_base_type, $user, $product->tva_tx, $product->price_min, 0, $product->tva_npr, 0, 0, array(), $product->default_vat_code, 'Preisbot' ); if ($result > 0) { $updated++; $diff = $newPrice - $currentPrice; $diffPercent = $currentPrice > 0 ? round(($diff / $currentPrice) * 100, 2) : 0; $priceChanges[] = array( 'ref' => $obj->ref, 'label' => $obj->label, 'old_price' => $currentPrice, 'new_price' => $newPrice, 'diff' => $diff, 'diff_percent' => $diffPercent, 'margin' => $margin, 'buy_price' => $buyPrice ); $this->output .= "Produkt ".$obj->ref.": ".$currentPrice." -> ".$newPrice." EUR (Aufschlag: ".$margin."%)\n"; } else { $errors++; $this->errors[] = "Fehler bei Produkt ".$obj->ref.": ".$product->error; } } $this->output .= "\n--- Zusammenfassung ---\n"; $this->output .= "Aktualisiert: ".$updated."\n"; $this->output .= "Übersprungen: ".$skipped."\n"; $this->output .= "Fehler: ".$errors."\n"; // Benachrichtigung und Mail nur wenn es Änderungen gab if ($updated > 0) { try { $this->sendNotification($priceChanges, $updated, $errors); } catch (Exception $e) { $this->output .= "Warnung: GlobalNotify fehlgeschlagen: ".$e->getMessage()."\n"; } try { $this->sendMailReport($priceChanges, $updated, $skipped, $errors); } catch (Exception $e) { $this->output .= "Warnung: Mail-Versand fehlgeschlagen: ".$e->getMessage()."\n"; } } return ($errors > 0) ? -1 : 0; } catch (Exception $e) { $this->error = 'PreisBot Exception: '.$e->getMessage(); $this->output .= 'FEHLER: '.$e->getMessage()."\n"; return -1; } } /** * Get supplier price for product * * @param int $productId Product ID * @param string $priceSource Price source (cheapest, newest) * @return float Supplier unitprice or 0 if not found */ private function getSupplierPrice($productId, $priceSource) { $orderBy = 'pfp.unitprice ASC'; // cheapest if ($priceSource === 'newest') { $orderBy = 'pfp.tms DESC'; } $sql = "SELECT pfp.unitprice"; $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price as pfp"; $sql .= " WHERE pfp.fk_product = ".((int) $productId); $sql .= " AND pfp.status = 1"; // Nur aktive Preise $sql .= " AND pfp.unitprice > 0"; $sql .= " ORDER BY ".$orderBy; $sql .= " LIMIT 1"; $resql = $this->db->query($sql); if ($resql && $this->db->num_rows($resql) > 0) { $obj = $this->db->fetch_object($resql); return (float) $obj->unitprice; } return 0; } /** * Send GlobalNotify notification * * @param array $priceChanges Array of price changes * @param int $updated Number of updated products * @param int $errors Number of errors */ private function sendNotification($priceChanges, $updated, $errors) { // Prüfe ob GlobalNotify verfügbar ist $globalNotifyFile = dol_buildpath('/globalnotify/class/globalnotify.class.php', 0); if (!file_exists($globalNotifyFile) || !isModEnabled('globalnotify')) { return; } require_once $globalNotifyFile; $title = $updated.' Produktpreise aktualisiert'; $message = "PreisBot hat ".$updated." Verkaufspreise angepasst.\n\n"; // Zeige die ersten 5 Änderungen in der Notification $count = 0; foreach ($priceChanges as $change) { if ($count >= 5) { $message .= "\n... und ".($updated - 5)." weitere"; break; } $diffSign = $change['diff'] >= 0 ? '+' : ''; $message .= $change['ref'].": ".number_format($change['old_price'], 2, ',', '.')." -> ".number_format($change['new_price'], 2, ',', '.')." EUR (".$diffSign.number_format($change['diff'], 2, ',', '.')." EUR)\n"; $count++; } if ($errors > 0) { GlobalNotify::warning('preisbot', $title, $message); } else { GlobalNotify::info('preisbot', $title, $message); } } /** * Send email report with detailed price changes * * @param array $priceChanges Array of price changes * @param int $updated Number of updated products * @param int $skipped Number of skipped products * @param int $errors Number of errors */ private function sendMailReport($priceChanges, $updated, $skipped, $errors) { global $conf, $user; $mailTo = getDolGlobalString('PREISBOT_MAIL_TO'); if (empty($mailTo)) { return; } require_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php'; $subject = '[PreisBot] '.$updated.' Produktpreise aktualisiert - '.dol_print_date(dol_now(), 'day'); // HTML Mail erstellen $body = ''; $body .= '

PreisBot - Preisanpassungen vom '.dol_print_date(dol_now(), 'dayhour').'

'; // Zusammenfassung $body .= '
'; $body .= 'Zusammenfassung:
'; $body .= 'Aktualisiert: '.$updated.'
'; $body .= 'Übersprungen: '.$skipped.'
'; if ($errors > 0) { $body .= 'Fehler: '.$errors.'
'; } $body .= '
'; // Tabelle mit Preisänderungen $body .= '

Details der Preisänderungen

'; $body .= ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; foreach ($priceChanges as $change) { $diffClass = $change['diff'] >= 0 ? 'positive' : 'negative'; $diffSign = $change['diff'] >= 0 ? '+' : ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; $body .= ''; } $body .= '
ReferenzBezeichnungEK-PreisAufschlagAlter VKNeuer VKDifferenz%
'.$change['ref'].''.dol_trunc($change['label'], 40).''.number_format($change['buy_price'], 2, ',', '.').' €'.number_format($change['margin'], 1, ',', '.').' %'.number_format($change['old_price'], 2, ',', '.').' €'.number_format($change['new_price'], 2, ',', '.').' €'.$diffSign.number_format($change['diff'], 2, ',', '.').' €'.$diffSign.number_format($change['diff_percent'], 1, ',', '.').' %
'; $body .= '

Diese E-Mail wurde automatisch vom PreisBot-Modul generiert.'; $body .= ''; $from = getDolGlobalString('MAIN_MAIL_EMAIL_FROM', getDolGlobalString('MAIN_INFO_SOCIETE_MAIL', 'noreply@example.com')); $mail = new CMailFile( $subject, $mailTo, $from, $body, array(), // filenames array(), // mimetypes array(), // filenames2 '', // cc '', // bcc 0, // deliveryreceipt 1 // msgishtml ); $mail->sendfile(); } }