* * 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 metallzuschlag/class/metallzuschlagapi.class.php * \ingroup metallzuschlag * \brief API-Client fuer Metallnotierungen + Cronjob-Methode */ /** * Klasse fuer Metallnotiz-API-Abfragen und Cronjob */ class MetallzuschlagApi { /** @var DoliDB */ public $db; /** @var string */ public $error = ''; /** @var string[] */ public $errors = array(); /** @var string */ public $output = ''; /** * Constructor * * @param DoliDB $db Database handler */ public function __construct($db) { $this->db = $db; } /** * Cronjob-Methode: Metallnotierungen abrufen und speichern * * @return int 0 bei Erfolg, -1 bei Fehler */ public function fetchMetalNotes() { global $conf, $langs; $langs->load('metallzuschlag@metallzuschlag'); dol_syslog("MetallzuschlagApi::fetchMetalNotes - Start", LOG_INFO); $baseUrl = getDolGlobalString('METALLZUSCHLAG_API_URL', 'https://www.sonepar.de/api/content/metalnote'); $today = date('Ymd'); $firstOfMonth = date('Ym01'); $results = array(); $hasError = false; // Tageswerte abrufen $dayData = $this->callApi($baseUrl.'/day?date='.$today); if ($dayData !== false && !empty($dayData['data'])) { foreach ($dayData['data'] as $entry) { $metal = $entry['nes']; $value = (float)$entry['nku']; $date = $entry['ndt']; $res = $this->saveNotation($date, $metal, $value, null, 'sonepar'); if ($res < 0) { $hasError = true; } else { $results[] = $metal.': '.$value.' EUR/100kg ('.$date.')'; } } } else { $this->output .= "Tageswerte: Keine Daten erhalten\n"; $hasError = true; } // Monatsdurchschnitt Vormonat abrufen $monthData = $this->callApi($baseUrl.'/previousmonth?date='.$firstOfMonth); if ($monthData !== false && !empty($monthData['data'])) { foreach ($monthData['data'] as $entry) { $metal = $entry['nes']; $avgValue = (float)$entry['nku']; // Monatsdurchschnitt auf den heutigen Tageseintrag aktualisieren $this->updateMonthAvg(date('Y-m-d'), $metal, $avgValue, 'sonepar'); $results[] = $metal.' Ø Vormonat: '.$avgValue.' EUR/100kg'; } } // Lieferanten-Extrafields aktualisieren $updatedSuppliers = $this->updateSupplierExtrafields($dayData); if ($updatedSuppliers > 0) { $results[] = $updatedSuppliers.' Lieferant(en) aktualisiert'; } if (!empty($results)) { $this->output = implode("\n", $results); } dol_syslog("MetallzuschlagApi::fetchMetalNotes - Fertig: ".implode(', ', $results), LOG_INFO); return $hasError ? -1 : 0; } /** * API-Aufruf durchfuehren * * @param string $url URL zum Abrufen * @return array|false Dekodierte JSON-Antwort oder false bei Fehler */ public function callApi($url) { dol_syslog("MetallzuschlagApi::callApi - URL: ".$url, LOG_DEBUG); $ch = curl_init(); curl_setopt_array($ch, array( CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_FOLLOWLOCATION => true, CURLOPT_HTTPHEADER => array( 'Accept: application/json', 'User-Agent: Dolibarr-Metallzuschlag/1.0', ), )); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); if ($response === false || !empty($curlError)) { $this->error = 'cURL-Fehler: '.$curlError; dol_syslog("MetallzuschlagApi::callApi - ".$this->error, LOG_ERR); return false; } if ($httpCode !== 200) { $this->error = 'HTTP '.$httpCode.' von '.$url; dol_syslog("MetallzuschlagApi::callApi - ".$this->error, LOG_ERR); return false; } $data = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { $this->error = 'JSON-Parse-Fehler: '.json_last_error_msg(); dol_syslog("MetallzuschlagApi::callApi - ".$this->error, LOG_ERR); return false; } if (empty($data['status']) || $data['status']['statuscode'] != 200) { $this->error = 'API-Fehler: Status '.($data['status']['statuscode'] ?? 'unbekannt'); dol_syslog("MetallzuschlagApi::callApi - ".$this->error, LOG_ERR); return false; } return $data; } /** * Notierung in DB speichern (INSERT oder UPDATE bei Duplikat) * * @param string $date Datum (Y-m-d) * @param string $metal Metall-Kuerzel (CU/AL) * @param float $value Wert in EUR/100kg * @param float|null $avg Monatsdurchschnitt (optional) * @param string $source Quelle * @return int 1 bei Erfolg, -1 bei Fehler */ public function saveNotation($date, $metal, $value, $avg = null, $source = 'sonepar') { $sql = "INSERT INTO ".$this->db->prefix()."metallzuschlag_history"; $sql .= " (date_notiz, metal, value, value_month_avg, source)"; $sql .= " VALUES ('".$this->db->escape($date)."',"; $sql .= " '".$this->db->escape($metal)."',"; $sql .= " ".((float)$value).","; $sql .= " ".($avg !== null ? ((float)$avg) : "NULL").","; $sql .= " '".$this->db->escape($source)."')"; $sql .= " ON DUPLICATE KEY UPDATE value = ".((float)$value); if ($avg !== null) { $sql .= ", value_month_avg = ".((float)$avg); } $result = $this->db->query($sql); if (!$result) { $this->error = 'DB-Fehler: '.$this->db->lasterror(); dol_syslog("MetallzuschlagApi::saveNotation - ".$this->error, LOG_ERR); return -1; } return 1; } /** * Monatsdurchschnitt auf bestehenden Eintrag aktualisieren * * @param string $date Datum * @param string $metal Metall * @param float $avg Durchschnittswert * @param string $source Quelle * @return int */ public function updateMonthAvg($date, $metal, $avg, $source = 'sonepar') { $sql = "UPDATE ".$this->db->prefix()."metallzuschlag_history"; $sql .= " SET value_month_avg = ".((float)$avg); $sql .= " WHERE date_notiz = '".$this->db->escape($date)."'"; $sql .= " AND metal = '".$this->db->escape($metal)."'"; $sql .= " AND source = '".$this->db->escape($source)."'"; return $this->db->query($sql) ? 1 : -1; } /** * Lieferanten-Extrafields mit aktuellen Werten aktualisieren * * @param array|false $dayData Tageswerte von der API * @return int Anzahl aktualisierter Lieferanten */ public function updateSupplierExtrafields($dayData) { if ($dayData === false || empty($dayData['data'])) { return 0; } // Werte aus API-Antwort extrahieren $values = array(); foreach ($dayData['data'] as $entry) { $values[$entry['nes']] = (float)$entry['nku']; } $cuValue = isset($values['CU']) ? $values['CU'] : null; $alValue = isset($values['AL']) ? $values['AL'] : null; $today = date('Y-m-d'); // Alle Lieferanten mit metallzuschlag_source != '' finden $sql = "SELECT fk_object, metallzuschlag_source"; $sql .= " FROM ".$this->db->prefix()."societe_extrafields"; $sql .= " WHERE metallzuschlag_source IS NOT NULL"; $sql .= " AND metallzuschlag_source != ''"; $sql .= " AND metallzuschlag_source != 'manuell'"; $resql = $this->db->query($sql); if (!$resql) { return 0; } $count = 0; while ($obj = $this->db->fetch_object($resql)) { $sets = array(); if ($cuValue !== null) { $sets[] = "metallzuschlag_cu = ".$cuValue; } if ($alValue !== null) { $sets[] = "metallzuschlag_al = ".$alValue; } $sets[] = "metallzuschlag_date = '".$this->db->escape($today)."'"; if (!empty($sets)) { $sqlUpdate = "UPDATE ".$this->db->prefix()."societe_extrafields"; $sqlUpdate .= " SET ".implode(", ", $sets); $sqlUpdate .= " WHERE fk_object = ".((int)$obj->fk_object); if ($this->db->query($sqlUpdate)) { $count++; } } } return $count; } /** * Aktuelle Notierung aus DB holen * * @param string $metal Metall (CU/AL) * @param string $source Quelle * @return object|null Objekt mit value, value_month_avg, date_notiz oder null */ public function getLatest($metal = 'CU', $source = 'sonepar') { $sql = "SELECT date_notiz, metal, value, value_month_avg, source"; $sql .= " FROM ".$this->db->prefix()."metallzuschlag_history"; $sql .= " WHERE metal = '".$this->db->escape($metal)."'"; $sql .= " AND source = '".$this->db->escape($source)."'"; $sql .= " ORDER BY date_notiz DESC LIMIT 1"; $resql = $this->db->query($sql); if ($resql && $this->db->num_rows($resql) > 0) { return $this->db->fetch_object($resql); } return null; } /** * Historie aus DB holen * * @param string $source Quelle * @param int $limit Anzahl Eintraege * @return array Array von Objekten */ public function getHistory($source = 'sonepar', $limit = 30) { $results = array(); $sql = "SELECT date_notiz, metal, value, value_month_avg, source"; $sql .= " FROM ".$this->db->prefix()."metallzuschlag_history"; $sql .= " WHERE source = '".$this->db->escape($source)."'"; $sql .= " ORDER BY date_notiz DESC, metal ASC"; $sql .= " LIMIT ".((int)$limit); $resql = $this->db->query($sql); if ($resql) { while ($obj = $this->db->fetch_object($resql)) { $results[] = $obj; } } return $results; } /** * Cronjob: Kupferzuschlag auf allen Einkaufspreisen neu berechnen * * Fuer alle Produkte mit Kupfergehalt > 0 werden die Einkaufspreise aktualisiert. * CU-Notiz: Lieferant-eigener Wert (metallzuschlag_cu) oder aktuellster aus History. * * @return int 0 bei Erfolg, -1 bei Fehler */ public function recalcAllKupferzuschlag() { global $langs; $langs->load('metallzuschlag@metallzuschlag'); dol_syslog("MetallzuschlagApi::recalcAllKupferzuschlag - Start", LOG_INFO); // Aktuellste CU-Notiz als Fallback $fallbackCU = 0; $latestCU = $this->getLatest('CU'); if ($latestCU) { $fallbackCU = (float) $latestCU->value; } if ($fallbackCU <= 0) { $this->error = 'Keine CU-Notierung vorhanden'; dol_syslog("MetallzuschlagApi::recalcAllKupferzuschlag - ".$this->error, LOG_ERR); return -1; } // Alle Produkte mit kupfergehalt > 0 $sql = "SELECT pe.fk_object, pe.kupfergehalt"; $sql .= " FROM ".$this->db->prefix()."product_extrafields pe"; $sql .= " WHERE pe.kupfergehalt IS NOT NULL AND pe.kupfergehalt > 0"; $resql = $this->db->query($sql); if (!$resql) { $this->error = 'DB-Fehler: '.$this->db->lasterror(); return -1; } $productCount = 0; $priceCount = 0; while ($product = $this->db->fetch_object($resql)) { $kupfergehalt = (float) $product->kupfergehalt; $productId = (int) $product->fk_object; // Alle Einkaufspreise dieses Produkts $sqlPrices = "SELECT pf.rowid, pf.fk_soc"; $sqlPrices .= " FROM ".$this->db->prefix()."product_fournisseur_price pf"; $sqlPrices .= " WHERE pf.fk_product = ".$productId; $resPrices = $this->db->query($sqlPrices); if (!$resPrices) { continue; } while ($price = $this->db->fetch_object($resPrices)) { // CU-Notiz des Lieferanten oder Fallback $cuNotiz = $this->getCUForSupplier((int) $price->fk_soc, $fallbackCU); if ($cuNotiz <= 0) { continue; } // Kupferzuschlag EUR/m = Kupfergehalt (kg/km) × CU (EUR/100kg) / 100.000 $kupferzuschlag = $kupfergehalt * $cuNotiz / 100000; // UPDATE oder INSERT $sqlUpd = "UPDATE ".$this->db->prefix()."product_fournisseur_price_extrafields"; $sqlUpd .= " SET kupferzuschlag = ".((float) $kupferzuschlag); $sqlUpd .= " WHERE fk_object = ".((int) $price->rowid); $resUpd = $this->db->query($sqlUpd); if ($resUpd && $this->db->affected_rows($resUpd) > 0) { $priceCount++; } else { $sqlIns = "INSERT INTO ".$this->db->prefix()."product_fournisseur_price_extrafields"; $sqlIns .= " (fk_object, kupferzuschlag) VALUES (".((int) $price->rowid).", ".((float) $kupferzuschlag).")"; if ($this->db->query($sqlIns)) { $priceCount++; } } } $productCount++; } $this->output = $productCount.' Produkte, '.$priceCount.' Einkaufspreise aktualisiert (CU: '.$fallbackCU.' EUR/100kg)'; dol_syslog("MetallzuschlagApi::recalcAllKupferzuschlag - ".$this->output, LOG_INFO); return 0; } /** * CU-Notiz fuer einen Lieferanten ermitteln * * @param int $socId Lieferanten-ID * @param float $fallbackCU Fallback-Wert aus History * @return float CU-Notiz in EUR/100kg */ public function getCUForSupplier($socId, $fallbackCU) { $sql = "SELECT metallzuschlag_cu"; $sql .= " FROM ".$this->db->prefix()."societe_extrafields"; $sql .= " WHERE fk_object = ".((int) $socId); $resql = $this->db->query($sql); if ($resql && $this->db->num_rows($resql) > 0) { $obj = $this->db->fetch_object($resql); if (!empty($obj->metallzuschlag_cu) && (float) $obj->metallzuschlag_cu > 0) { return (float) $obj->metallzuschlag_cu; } } return $fallbackCU; } /** * Verlaufsdaten fuer Chart aufbereiten * * @param int $days Zeitraum in Tagen * @param string $source Quelle * @return array Array mit 'labels', 'cu', 'al' fuer Chart.js */ public function getChartData($days = 90, $source = 'sonepar') { $result = array( 'labels' => array(), 'cu' => array(), 'al' => array(), ); $dateFrom = date('Y-m-d', strtotime('-'.$days.' days')); // Alle Daten im Zeitraum holen, nach Datum sortiert $sql = "SELECT date_notiz, metal, value"; $sql .= " FROM ".$this->db->prefix()."metallzuschlag_history"; $sql .= " WHERE source = '".$this->db->escape($source)."'"; $sql .= " AND date_notiz >= '".$this->db->escape($dateFrom)."'"; $sql .= " ORDER BY date_notiz ASC, metal ASC"; $resql = $this->db->query($sql); if (!$resql) { return $result; } // Daten nach Datum gruppieren $byDate = array(); while ($obj = $this->db->fetch_object($resql)) { $date = $obj->date_notiz; if (!isset($byDate[$date])) { $byDate[$date] = array('CU' => null, 'AL' => null); } $byDate[$date][$obj->metal] = (float) $obj->value; } foreach ($byDate as $date => $values) { $result['labels'][] = $date; $result['cu'][] = $values['CU']; $result['al'][] = $values['AL']; } return $result; } }