Replace $conf->global->MAIN_INFO_SOCIETE_MAIL with getDolGlobalString() to prevent fatal error when constant is not set. This caused the cronjob to hang on the productive server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
350 lines
10 KiB
PHP
Executable file
350 lines
10 KiB
PHP
Executable file
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* 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;
|
|
|
|
try {
|
|
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
|
|
|
$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 = '<html><head><style>
|
|
body { font-family: Arial, sans-serif; }
|
|
table { border-collapse: collapse; width: 100%; margin-top: 15px; }
|
|
th, td { border: 1px solid #ddd; padding: 8px; text-align: right; }
|
|
th { background-color: #4CAF50; color: white; }
|
|
tr:nth-child(even) { background-color: #f2f2f2; }
|
|
td:first-child, td:nth-child(2) { text-align: left; }
|
|
.positive { color: green; }
|
|
.negative { color: red; }
|
|
.summary { background-color: #f5f5f5; padding: 15px; margin-bottom: 20px; border-radius: 5px; }
|
|
</style></head><body>';
|
|
|
|
$body .= '<h2>PreisBot - Preisanpassungen vom '.dol_print_date(dol_now(), 'dayhour').'</h2>';
|
|
|
|
// Zusammenfassung
|
|
$body .= '<div class="summary">';
|
|
$body .= '<strong>Zusammenfassung:</strong><br>';
|
|
$body .= 'Aktualisiert: <strong>'.$updated.'</strong><br>';
|
|
$body .= 'Übersprungen: '.$skipped.'<br>';
|
|
if ($errors > 0) {
|
|
$body .= '<span style="color:red;">Fehler: '.$errors.'</span><br>';
|
|
}
|
|
$body .= '</div>';
|
|
|
|
// Tabelle mit Preisänderungen
|
|
$body .= '<h3>Details der Preisänderungen</h3>';
|
|
$body .= '<table>';
|
|
$body .= '<tr>';
|
|
$body .= '<th>Referenz</th>';
|
|
$body .= '<th>Bezeichnung</th>';
|
|
$body .= '<th>EK-Preis</th>';
|
|
$body .= '<th>Aufschlag</th>';
|
|
$body .= '<th>Alter VK</th>';
|
|
$body .= '<th>Neuer VK</th>';
|
|
$body .= '<th>Differenz</th>';
|
|
$body .= '<th>%</th>';
|
|
$body .= '</tr>';
|
|
|
|
foreach ($priceChanges as $change) {
|
|
$diffClass = $change['diff'] >= 0 ? 'positive' : 'negative';
|
|
$diffSign = $change['diff'] >= 0 ? '+' : '';
|
|
|
|
$body .= '<tr>';
|
|
$body .= '<td>'.$change['ref'].'</td>';
|
|
$body .= '<td>'.dol_trunc($change['label'], 40).'</td>';
|
|
$body .= '<td>'.number_format($change['buy_price'], 2, ',', '.').' €</td>';
|
|
$body .= '<td>'.number_format($change['margin'], 1, ',', '.').' %</td>';
|
|
$body .= '<td>'.number_format($change['old_price'], 2, ',', '.').' €</td>';
|
|
$body .= '<td><strong>'.number_format($change['new_price'], 2, ',', '.').' €</strong></td>';
|
|
$body .= '<td class="'.$diffClass.'">'.$diffSign.number_format($change['diff'], 2, ',', '.').' €</td>';
|
|
$body .= '<td class="'.$diffClass.'">'.$diffSign.number_format($change['diff_percent'], 1, ',', '.').' %</td>';
|
|
$body .= '</tr>';
|
|
}
|
|
|
|
$body .= '</table>';
|
|
$body .= '<br><br><small>Diese E-Mail wurde automatisch vom PreisBot-Modul generiert.</small>';
|
|
$body .= '</body></html>';
|
|
|
|
$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();
|
|
}
|
|
}
|