feat: Kabel-Kupferzuschlag-Diagramm auf Dashboard
- Neues Diagramm zeigt Kupferzuschlag-Verlauf pro Kabel - Kabel-Auswahl mit Checkboxen (alle oder einzelne) - Modus-Auswahl: EUR/m oder Gesamtbetrag (mit Mindestmenge) - API: getProductsWithKupfergehalt(), getCableChartData() - Version auf 1.3 erhöht Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2e2cb5b710
commit
b90f13da34
10 changed files with 313 additions and 1 deletions
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
|
|
@ -1,5 +1,14 @@
|
|||
# Changelog - Metallzuschlag Modul
|
||||
|
||||
## 1.3 (2026-02-24)
|
||||
|
||||
### Neu
|
||||
- Dashboard: Kabel-Kupferzuschlag-Diagramm mit zeitgenauem Verlauf
|
||||
- Kabel-Auswahl: Einzelne oder alle Kabel ein-/ausblenden
|
||||
- Modus-Auswahl: EUR/m (Stueckpreis) oder Gesamtbetrag (mit Mindestmenge)
|
||||
- API: getProductsWithKupfergehalt() - alle Kabel mit Kupfergehalt
|
||||
- API: getCableChartData() - Kupferzuschlag-Verlauf berechnen
|
||||
|
||||
## 1.2 (2026-02-24)
|
||||
|
||||
### Geaendert
|
||||
|
|
|
|||
134
class/metallzuschlagapi.class.php
Normal file → Executable file
134
class/metallzuschlagapi.class.php
Normal file → Executable file
|
|
@ -489,4 +489,138 @@ class MetallzuschlagApi
|
|||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Produkte mit Kupfergehalt > 0 holen
|
||||
*
|
||||
* @return array Array von Objekten mit id, ref, label, kupfergehalt
|
||||
*/
|
||||
public function getProductsWithKupfergehalt()
|
||||
{
|
||||
$results = array();
|
||||
|
||||
$sql = "SELECT p.rowid, p.ref, p.label, pe.kupfergehalt";
|
||||
$sql .= " FROM ".$this->db->prefix()."product p";
|
||||
$sql .= " INNER JOIN ".$this->db->prefix()."product_extrafields pe ON pe.fk_object = p.rowid";
|
||||
$sql .= " WHERE pe.kupfergehalt IS NOT NULL AND pe.kupfergehalt > 0";
|
||||
$sql .= " ORDER BY p.ref ASC";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$results[] = $obj;
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kupferzuschlag-Verlauf fuer Kabel berechnen
|
||||
*
|
||||
* @param int $days Zeitraum in Tagen
|
||||
* @param array $productIds Produkt-IDs (leer = alle mit Kupfergehalt)
|
||||
* @param string $mode 'per_meter' oder 'total' (mit Mindestmenge)
|
||||
* @param string $source Quelle fuer CU-Notierungen
|
||||
* @return array ['labels' => [...], 'products' => [id => ['label' => name, 'data' => [...]]]]
|
||||
*/
|
||||
public function getCableChartData($days = 90, $productIds = array(), $mode = 'per_meter', $source = 'sonepar')
|
||||
{
|
||||
$result = array(
|
||||
'labels' => array(),
|
||||
'products' => array(),
|
||||
);
|
||||
|
||||
// CU-Notierungen im Zeitraum holen
|
||||
$dateFrom = date('Y-m-d', strtotime('-'.$days.' days'));
|
||||
|
||||
$sql = "SELECT date_notiz, value";
|
||||
$sql .= " FROM ".$this->db->prefix()."metallzuschlag_history";
|
||||
$sql .= " WHERE metal = 'CU'";
|
||||
$sql .= " AND source = '".$this->db->escape($source)."'";
|
||||
$sql .= " AND date_notiz >= '".$this->db->escape($dateFrom)."'";
|
||||
$sql .= " ORDER BY date_notiz ASC";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$cuByDate = array();
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$cuByDate[$obj->date_notiz] = (float) $obj->value;
|
||||
$result['labels'][] = $obj->date_notiz;
|
||||
}
|
||||
|
||||
if (empty($result['labels'])) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Produkte mit Kupfergehalt holen
|
||||
$sqlProd = "SELECT p.rowid, p.ref, p.label, pe.kupfergehalt";
|
||||
$sqlProd .= " FROM ".$this->db->prefix()."product p";
|
||||
$sqlProd .= " INNER JOIN ".$this->db->prefix()."product_extrafields pe ON pe.fk_object = p.rowid";
|
||||
$sqlProd .= " WHERE pe.kupfergehalt IS NOT NULL AND pe.kupfergehalt > 0";
|
||||
|
||||
if (!empty($productIds)) {
|
||||
$sqlProd .= " AND p.rowid IN (".implode(',', array_map('intval', $productIds)).")";
|
||||
}
|
||||
|
||||
$sqlProd .= " ORDER BY p.ref ASC";
|
||||
|
||||
$resProd = $this->db->query($sqlProd);
|
||||
if (!$resProd) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
while ($product = $this->db->fetch_object($resProd)) {
|
||||
$kupfergehalt = (float) $product->kupfergehalt;
|
||||
$productId = (int) $product->rowid;
|
||||
|
||||
// Mindestmenge ermitteln (kleinste quantity aus Einkaufspreisen)
|
||||
$quantity = 1;
|
||||
if ($mode === 'total') {
|
||||
$sqlQty = "SELECT MIN(quantity) as min_qty";
|
||||
$sqlQty .= " FROM ".$this->db->prefix()."product_fournisseur_price";
|
||||
$sqlQty .= " WHERE fk_product = ".$productId;
|
||||
$sqlQty .= " AND quantity > 0";
|
||||
|
||||
$resQty = $this->db->query($sqlQty);
|
||||
if ($resQty && $this->db->num_rows($resQty) > 0) {
|
||||
$objQty = $this->db->fetch_object($resQty);
|
||||
if ($objQty->min_qty > 0) {
|
||||
$quantity = (float) $objQty->min_qty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kupferzuschlag fuer jeden Tag berechnen
|
||||
$data = array();
|
||||
foreach ($result['labels'] as $date) {
|
||||
$cuNotiz = isset($cuByDate[$date]) ? $cuByDate[$date] : 0;
|
||||
if ($cuNotiz > 0) {
|
||||
// Kupferzuschlag = Kupfergehalt × CU / 100.000 × Menge
|
||||
$data[] = round($kupfergehalt * $cuNotiz / 100000 * $quantity, 2);
|
||||
} else {
|
||||
$data[] = null;
|
||||
}
|
||||
}
|
||||
|
||||
$label = $product->ref;
|
||||
if (!empty($product->label)) {
|
||||
$label .= ' - '.$product->label;
|
||||
}
|
||||
if ($mode === 'total' && $quantity > 1) {
|
||||
$label .= ' ('.$quantity.'m)';
|
||||
}
|
||||
|
||||
$result['products'][$productId] = array(
|
||||
'label' => $label,
|
||||
'data' => $data,
|
||||
'kupfergehalt' => $kupfergehalt,
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class modMetallzuschlag extends DolibarrModules
|
|||
$this->descriptionlong = "MetallzuschlagDescriptionLong";
|
||||
$this->editor_name = 'Alles Watt laeuft';
|
||||
$this->editor_url = '';
|
||||
$this->version = '1.2';
|
||||
$this->version = '1.3';
|
||||
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
|
||||
$this->picto = 'fa-coins';
|
||||
|
||||
|
|
|
|||
0
core/triggers/interface_99_modMetallzuschlag_MetallzuschlagTriggers.class.php
Normal file → Executable file
0
core/triggers/interface_99_modMetallzuschlag_MetallzuschlagTriggers.class.php
Normal file → Executable file
9
langs/de_DE/metallzuschlag.lang
Normal file → Executable file
9
langs/de_DE/metallzuschlag.lang
Normal file → Executable file
|
|
@ -80,3 +80,12 @@ MetallzuschlagChartAL = Aluminium (AL) €/100kg
|
|||
MetallzuschlagLast30Days = Letzte 30 Tage
|
||||
MetallzuschlagLast90Days = Letzte 90 Tage
|
||||
MetallzuschlagLast365Days = Letzte 365 Tage
|
||||
|
||||
# Kabel-Diagramm
|
||||
MetallzuschlagCableChart = Kupferzuschlag Kabel
|
||||
MetallzuschlagSelectCables = Kabel auswählen
|
||||
MetallzuschlagAllCables = Alle Kabel
|
||||
MetallzuschlagPerMeter = EUR/m
|
||||
MetallzuschlagTotalAmount = Gesamtbetrag
|
||||
MetallzuschlagPerMeterEUR = Kupferzuschlag (EUR/m)
|
||||
MetallzuschlagTotalAmountEUR = Kupferzuschlag (EUR)
|
||||
|
|
|
|||
|
|
@ -79,3 +79,12 @@ MetallzuschlagChartAL = Aluminum (AL) €/100kg
|
|||
MetallzuschlagLast30Days = Last 30 days
|
||||
MetallzuschlagLast90Days = Last 90 days
|
||||
MetallzuschlagLast365Days = Last 365 days
|
||||
|
||||
# Cable chart
|
||||
MetallzuschlagCableChart = Cable copper surcharge
|
||||
MetallzuschlagSelectCables = Select cables
|
||||
MetallzuschlagAllCables = All cables
|
||||
MetallzuschlagPerMeter = EUR/m
|
||||
MetallzuschlagTotalAmount = Total amount
|
||||
MetallzuschlagPerMeterEUR = Copper surcharge (EUR/m)
|
||||
MetallzuschlagTotalAmountEUR = Copper surcharge (EUR)
|
||||
|
|
|
|||
|
|
@ -285,6 +285,157 @@ if (!empty($chartData['labels'])) {
|
|||
print '</script>';
|
||||
}
|
||||
|
||||
// Kabel-Diagramm: Kupferzuschlag pro Kabel
|
||||
$cablesWithKupfer = $api->getProductsWithKupfergehalt();
|
||||
|
||||
if (!empty($cablesWithKupfer) && !empty($chartData['labels'])) {
|
||||
print '<br>';
|
||||
|
||||
// Parameter auslesen
|
||||
$cableIds = GETPOST('cable_ids', 'array');
|
||||
$cableMode = GETPOST('cable_mode', 'alpha') ?: 'per_meter';
|
||||
|
||||
// URL-Basis fuer Links
|
||||
$baseUrl = $_SERVER["PHP_SELF"].'?chart_days='.$chartDays;
|
||||
|
||||
// Kabel-Auswahl und Modus-Buttons
|
||||
$cableButtons = '';
|
||||
|
||||
// Modus-Buttons
|
||||
$modePerMeter = ($cableMode == 'per_meter') ? ' butActionActive' : '';
|
||||
$modeTotal = ($cableMode == 'total') ? ' butActionActive' : '';
|
||||
$cableButtons .= '<a class="butAction'.$modePerMeter.' small" href="'.$baseUrl.'&cable_mode=per_meter'.(!empty($cableIds) ? '&cable_ids[]='.implode('&cable_ids[]=', $cableIds) : '').'">'.$langs->trans("MetallzuschlagPerMeter").'</a> ';
|
||||
$cableButtons .= '<a class="butAction'.$modeTotal.' small" href="'.$baseUrl.'&cable_mode=total'.(!empty($cableIds) ? '&cable_ids[]='.implode('&cable_ids[]=', $cableIds) : '').'">'.$langs->trans("MetallzuschlagTotalAmount").'</a>';
|
||||
|
||||
print load_fiche_titre($langs->trans("MetallzuschlagCableChart"), $cableButtons, '');
|
||||
|
||||
// Kabel-Auswahl (Checkboxen)
|
||||
print '<div class="div-table-responsive-no-min" style="margin-bottom: 15px;">';
|
||||
print '<form method="get" action="'.$_SERVER["PHP_SELF"].'">';
|
||||
print '<input type="hidden" name="chart_days" value="'.$chartDays.'">';
|
||||
print '<input type="hidden" name="cable_mode" value="'.$cableMode.'">';
|
||||
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="4">'.$langs->trans("MetallzuschlagSelectCables").'</td>';
|
||||
print '</tr>';
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
$colCount = 0;
|
||||
foreach ($cablesWithKupfer as $cable) {
|
||||
$checked = (empty($cableIds) || in_array($cable->rowid, $cableIds)) ? ' checked' : '';
|
||||
print '<td style="width: 25%;">';
|
||||
print '<label>';
|
||||
print '<input type="checkbox" name="cable_ids[]" value="'.$cable->rowid.'"'.$checked.'> ';
|
||||
print dol_escape_htmltag($cable->ref);
|
||||
if (!empty($cable->label)) {
|
||||
print ' <span class="opacitymedium">('.dol_trunc($cable->label, 30).')</span>';
|
||||
}
|
||||
print '</label>';
|
||||
print '</td>';
|
||||
|
||||
$colCount++;
|
||||
if ($colCount % 4 == 0) {
|
||||
print '</tr><tr class="oddeven">';
|
||||
}
|
||||
}
|
||||
|
||||
// Leere Zellen auffuellen
|
||||
while ($colCount % 4 != 0) {
|
||||
print '<td></td>';
|
||||
$colCount++;
|
||||
}
|
||||
|
||||
print '</tr>';
|
||||
print '<tr class="oddeven">';
|
||||
print '<td colspan="4">';
|
||||
print '<button type="submit" class="butAction small">'.$langs->trans("Refresh").'</button> ';
|
||||
print '<a class="butAction small" href="'.$baseUrl.'&cable_mode='.$cableMode.'">'.$langs->trans("MetallzuschlagAllCables").'</a>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
print '</table>';
|
||||
print '</form>';
|
||||
print '</div>';
|
||||
|
||||
// Kabel-Chart-Daten holen
|
||||
$cableChartData = $api->getCableChartData($chartDays, $cableIds, $cableMode);
|
||||
|
||||
if (!empty($cableChartData['products'])) {
|
||||
print '<div style="max-width: 100%; height: 400px;">';
|
||||
print '<canvas id="cableChart"></canvas>';
|
||||
print '</div>';
|
||||
|
||||
// Farben fuer Kabel (verschiedene Farben)
|
||||
$colors = array(
|
||||
'#e6194B', '#3cb44b', '#ffe119', '#4363d8', '#f58231',
|
||||
'#911eb4', '#42d4f4', '#f032e6', '#bfef45', '#fabed4',
|
||||
'#469990', '#dcbeff', '#9A6324', '#fffac8', '#800000',
|
||||
);
|
||||
|
||||
// Datasets fuer Chart.js vorbereiten
|
||||
$jsLabels = json_encode($cableChartData['labels']);
|
||||
$datasets = array();
|
||||
$colorIndex = 0;
|
||||
|
||||
foreach ($cableChartData['products'] as $productId => $productData) {
|
||||
$color = $colors[$colorIndex % count($colors)];
|
||||
$datasets[] = array(
|
||||
'label' => $productData['label'],
|
||||
'data' => $productData['data'],
|
||||
'borderColor' => $color,
|
||||
'backgroundColor' => 'transparent',
|
||||
'borderWidth' => 2,
|
||||
'tension' => 0.3,
|
||||
'spanGaps' => true,
|
||||
);
|
||||
$colorIndex++;
|
||||
}
|
||||
|
||||
$jsDatasets = json_encode($datasets);
|
||||
$yAxisLabel = ($cableMode == 'total') ? $langs->trans("MetallzuschlagTotalAmountEUR") : $langs->trans("MetallzuschlagPerMeterEUR");
|
||||
|
||||
print '<script>';
|
||||
print 'document.addEventListener("DOMContentLoaded", function() {';
|
||||
print ' var ctx = document.getElementById("cableChart").getContext("2d");';
|
||||
print ' new Chart(ctx, {';
|
||||
print ' type: "line",';
|
||||
print ' data: {';
|
||||
print ' labels: '.$jsLabels.',';
|
||||
print ' datasets: '.$jsDatasets;
|
||||
print ' },';
|
||||
print ' options: {';
|
||||
print ' responsive: true,';
|
||||
print ' maintainAspectRatio: false,';
|
||||
print ' interaction: { mode: "index", intersect: false },';
|
||||
print ' plugins: {';
|
||||
print ' tooltip: {';
|
||||
print ' callbacks: {';
|
||||
print ' label: function(ctx) {';
|
||||
print ' return ctx.dataset.label + ": " + (ctx.parsed.y !== null ? ctx.parsed.y.toFixed(2) : "-") + " €";';
|
||||
print ' }';
|
||||
print ' }';
|
||||
print ' }';
|
||||
print ' },';
|
||||
print ' scales: {';
|
||||
print ' x: {';
|
||||
print ' ticks: { maxTicksLimit: 12 }';
|
||||
print ' },';
|
||||
print ' y: {';
|
||||
print ' type: "linear",';
|
||||
print ' position: "left",';
|
||||
print ' title: { display: true, text: "'.dol_escape_js($yAxisLabel).'" },';
|
||||
print ' ticks: { callback: function(v) { return v.toFixed(2) + " €"; } }';
|
||||
print ' }';
|
||||
print ' }';
|
||||
print ' }';
|
||||
print ' });';
|
||||
print '});';
|
||||
print '</script>';
|
||||
} else {
|
||||
print '<div class="opacitymedium">'.$langs->trans("MetallzuschlagNoData").'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Verlaufstabelle
|
||||
print '<br>';
|
||||
print load_fiche_titre($langs->trans("MetallzuschlagHistory"), '', '');
|
||||
|
|
|
|||
0
sql/llx_metallzuschlag_history.key.sql
Normal file → Executable file
0
sql/llx_metallzuschlag_history.key.sql
Normal file → Executable file
0
sql/llx_metallzuschlag_history.sql
Normal file → Executable file
0
sql/llx_metallzuschlag_history.sql
Normal file → Executable file
Loading…
Reference in a new issue