diff --git a/dist/ROI.php b/dist/ROI.php new file mode 100644 index 0000000..6a6854d --- /dev/null +++ b/dist/ROI.php @@ -0,0 +1,932 @@ +getTimestamp(); +} + +function tsToYmd(int $ts): string { + return (new DateTime('@' . $ts))->setTimezone(new DateTimeZone('Europe/Berlin'))->format('Y-m-d'); +} + +function base64Json(array $data): string { + return base64_encode(json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); +} + +function apiJsonDecode($raw) { + // requestEpiApi kann je nach Implementierung string ODER Stream liefern + if (is_object($raw)) { + // Guzzle Stream etc. + if (method_exists($raw, 'getContents')) { + $raw = $raw->getContents(); + } else { + $raw = (string)$raw; + } + } + if (!is_string($raw)) $raw = (string)$raw; + $obj = json_decode($raw); + return $obj; +} + +/* ========================= + API Caches + ========================= */ + +$productCache = []; // [pk => productObj] +$bundleLeafCache = []; // [bundlePk => [leafPk => amount]] +$rentPriceCache = []; // [pk => float] + +function getProduct(Epirent $Epi, int $productPk) { + global $productCache; + if ($productPk <= 0) return null; + if (isset($productCache[$productPk])) return $productCache[$productPk]; + + $res = apiJsonDecode($Epi->requestEpiApi('/v1/product/' . $productPk . '?cl=' . Epirent_Mandant)); + $prod = ($res && ($res->success ?? false) && !empty($res->payload[0])) ? $res->payload[0] : null; + $productCache[$productPk] = $prod; + return $prod; +} + +function getRentPrice(Epirent $Epi, int $productPk): float { + global $rentPriceCache; + if (isset($rentPriceCache[$productPk])) return $rentPriceCache[$productPk]; + + $p = getProduct($Epi, $productPk); + $price = 0.0; + if ($p && isset($p->pricing) && isset($p->pricing->price_rent)) { + $price = (float)$p->pricing->price_rent; + } + $rentPriceCache[$productPk] = $price; + return $price; +} + +/** + * Bundle-Komponenten rekursiv auflösen. + * Ergebnis: [leafProductPk => amount] + * + * WICHTIG (Fix für "UJ90 hat (0)"): + * - NUR virtuelle Produkte (is_virtual==true) werden expandiert. + * - Nicht-virtuelle Produkte zählen IMMER als leaf – auch wenn sie materials haben (Zubehör/Case/etc.) + * + * - bevorzugt materials_ext.rent_fix (weil rent_fix sauberer ist) + * - fallback materials + * - Cycle-Guard über $stack + */ +function resolveBundleLeafMap(Epirent $Epi, int $productPk, array &$stack = []): array { + global $bundleLeafCache; + + if ($productPk <= 0) return []; + if (isset($bundleLeafCache[$productPk])) return $bundleLeafCache[$productPk]; + + if (isset($stack[$productPk])) { + // cycle detected + return []; + } + $stack[$productPk] = true; + + $p = getProduct($Epi, $productPk); + if (!$p) { + unset($stack[$productPk]); + return []; + } + + // ✅ FIX: Nur virtuelle Produkte expandieren + $isVirtual = (bool)($p->is_virtual ?? false); + if (!$isVirtual) { + unset($stack[$productPk]); + $bundleLeafCache[$productPk] = [$productPk => 1.0]; + return $bundleLeafCache[$productPk]; + } + + // Komponentenliste auslesen (nur für virtuelle Produkte) + $mats = []; + + // materials_ext.rent_fix bevorzugen + if (!empty($p->materials_ext) && !empty($p->materials_ext->rent_fix) && is_array($p->materials_ext->rent_fix)) { + $mats = $p->materials_ext->rent_fix; + } elseif (!empty($p->materials) && is_array($p->materials)) { + $mats = $p->materials; + } + + // Wenn keine Komponenten => leaf + if (empty($mats)) { + unset($stack[$productPk]); + $bundleLeafCache[$productPk] = [$productPk => 1.0]; + return $bundleLeafCache[$productPk]; + } + + $leaf = []; + foreach ($mats as $m) { + $childPk = (int)($m->mat_product_pk ?? 0); + $amt = (float)($m->amount ?? 1); + if ($childPk <= 0 || $amt <= 0) continue; + + // rekursiv + $childLeaf = resolveBundleLeafMap($Epi, $childPk, $stack); + foreach ($childLeaf as $leafPk => $leafAmt) { + if (!isset($leaf[$leafPk])) $leaf[$leafPk] = 0.0; + $leaf[$leafPk] += $amt * (float)$leafAmt; + } + } + + unset($stack[$productPk]); + + // falls aus irgendeinem Grund leer => als leaf behandeln + if (empty($leaf)) $leaf = [$productPk => 1.0]; + + $bundleLeafCache[$productPk] = $leaf; + return $leaf; +} + +/** + * Bundle-Umsatz anteilig auf leaf-Produkte verteilen. + * Gewichtung: rentPrice(leaf) * amount + * Fallback: gleichverteilt nach amount + */ +function allocateBundleRevenue(Epirent $Epi, int $bundlePk, float $bundleRevenueNet): array { + $leafMap = resolveBundleLeafMap($Epi, $bundlePk); + if (empty($leafMap)) return []; + + $weights = []; + $sumW = 0.0; + + foreach ($leafMap as $leafPk => $amt) { + $rp = getRentPrice($Epi, (int)$leafPk); + $w = $rp * (float)$amt; + $weights[$leafPk] = $w; + $sumW += $w; + } + + // fallback: sumW=0 => gleich nach amount + if ($sumW <= 0.0) { + $sumAmt = array_sum($leafMap); + if ($sumAmt <= 0.0) $sumAmt = (float)count($leafMap); + $alloc = []; + foreach ($leafMap as $leafPk => $amt) { + $alloc[$leafPk] = $bundleRevenueNet * ((float)$amt / $sumAmt); + } + return $alloc; + } + + $alloc = []; + foreach ($weights as $leafPk => $w) { + $alloc[$leafPk] = $bundleRevenueNet * ($w / $sumW); + } + return $alloc; +} + +/* ========================= + Journal Cache + ========================= */ + +$journalCache = []; +function getJournalByChapter(Epirent $Epi, int $chapterId): array { + global $journalCache; + + if ($chapterId <= 0) return []; + if (isset($journalCache[$chapterId])) return $journalCache[$chapterId]; + + $url = '/v1/journal/filter?chid=' . $chapterId . '&cl=' . Epirent_Mandant; + $res = apiJsonDecode($Epi->requestEpiApi($url)); + $payload = ($res && ($res->success ?? false) && isset($res->payload) && is_array($res->payload)) ? $res->payload : []; + + $journalCache[$chapterId] = $payload; + return $payload; +} + +/* ========================= + Interval aggregation (Peak/Histogram) + ========================= */ + +// eventsDirect[productPk][ymd] += deltaQty +$eventsDirect = []; +$eventsIncl = []; // direct + bundle + +function addIntervalEvent(array &$events, int $productPk, string $startYmd, string $endYmd, float $qty): void { + if ($productPk <= 0) return; + $startYmd = safeYmd($startYmd); + $endYmd = safeYmd($endYmd); + if (!$startYmd || !$endYmd) return; + if ($qty == 0.0) return; + + $startTs = ymdToTs($startYmd); + $endTs = ymdToTs($endYmd); + if ($endTs < $startTs) return; + + // end+1 day + $endPlusTs = $endTs + 86400; + $endPlusYmd = tsToYmd($endPlusTs); + + if (!isset($events[$productPk])) $events[$productPk] = []; + if (!isset($events[$productPk][$startYmd])) $events[$productPk][$startYmd] = 0.0; + if (!isset($events[$productPk][$endPlusYmd])) $events[$productPk][$endPlusYmd] = 0.0; + + $events[$productPk][$startYmd] += $qty; + $events[$productPk][$endPlusYmd] -= $qty; +} + +/** + * Aus Events Peak + Histogramm berechnen: + * return ['peak'=>int, 'hist'=>[level=>days]] + */ +function computePeakAndHistogram(array $eventMap): array { + if (empty($eventMap)) return ['peak' => 0, 'hist' => []]; + + ksort($eventMap); + $dates = array_keys($eventMap); + + $level = 0.0; + $peak = 0.0; + $hist = []; // [intLevel => floatDays] + + for ($i = 0; $i < count($dates); $i++) { + $d = $dates[$i]; + $level += (float)$eventMap[$d]; + if ($level < 0) $level = 0; + + $intLevel = (int)round($level); + if ($intLevel > $peak) $peak = $intLevel; + + // segment length until next date + if ($i < count($dates) - 1) { + $dTs = ymdToTs($d); + $nTs = ymdToTs($dates[$i + 1]); + $days = ($nTs - $dTs) / 86400.0; + if ($days > 0 && $intLevel > 0) { + if (!isset($hist[$intLevel])) $hist[$intLevel] = 0.0; + $hist[$intLevel] += $days; + } + } + } + + ksort($hist); + // hist days runden + foreach ($hist as $k => $v) $hist[$k] = (int)round($v); + + return ['peak' => (int)$peak, 'hist' => $hist]; +} + +/* ========================= + Data aggregation + ========================= */ + +$rows = []; // Debug rows (HTML only, no excel) +$pivot = []; // direct revenue per year +$pivotB = []; // bundle revenue per year (alloc to leaf) +$meta = []; // product meta [pk=>['product_no'=>..,'title'=>..]] + +$allYears = []; + +function ensureMeta(Epirent $Epi, int $productPk, int $productNo, string $title): void { + global $meta; + if (!isset($meta[$productPk])) { + $p = getProduct($Epi, $productPk); + $meta[$productPk] = [ + 'product_pk' => $productPk, + 'product_no' => $productNo ?: (int)($p->product_no ?? 0), + 'title' => $title ?: (string)($p->name ?? ''), + ]; + } +} + +function addRevenue(array &$pivotRef, int $productPk, int $year, float $revenueNet): void { + if ($productPk <= 0 || !$year) return; + if (!isset($pivotRef[$productPk])) $pivotRef[$productPk] = []; + if (!isset($pivotRef[$productPk][$year])) $pivotRef[$productPk][$year] = 0.0; + $pivotRef[$productPk][$year] += $revenueNet; +} + +/** + * Eine Artikelzeile (aus Journal ODER direkt) verarbeiten + */ +function processLineItem( + Epirent $Epi, + array &$rows, + int $invoicePk, + string $invoiceDate, + int $invoiceYear, + ?int $chapterId, + object $li, + bool $isFromJournal +): void { + global $pivot, $pivotB, $allYears, $eventsDirect, $eventsIncl; + + $type = (int)($li->type ?? -1); + $productPk = (int)($li->product_pk ?? 0); + if ($type !== 0 || $productPk <= 0) return; // nur echte Artikelpositionen + + $title = (string)($li->title ?? ''); + $productNo = (int)($li->product_no ?? 0); + + $qty = (float)($li->amount_total ?? 0); + if ($qty <= 0) $qty = 0.0; + + // Zeitraum (für Peak/Histogramm) + $dateStart = (string)($li->date_start ?? ''); + $dateEnd = (string)($li->date_end ?? ''); + $dateStart = safeYmd($dateStart) ?: null; + $dateEnd = safeYmd($dateEnd) ?: null; + + // Umsatz netto (für Pivot) + $revenueNet = (float)($li->sum_total_net ?? 0); + + ensureMeta($Epi, $productPk, $productNo, $title); + + // Jahr: AN RECHNUNGSDATUM gebunden (dein Wunsch) + $year = $invoiceYear; + if ($year) $allYears[$year] = true; + + // Direct Umsatz (auch wenn Zeitraum evtl. leer ist) + if ($revenueNet != 0.0 && $year) { + addRevenue($pivot, $productPk, $year, $revenueNet); + } + + // Direct Auslastung (nur wenn Zeitraum da) + if ($dateStart && $dateEnd && $qty > 0) { + addIntervalEvent($eventsDirect, $productPk, $dateStart, $dateEnd, $qty); + addIntervalEvent($eventsIncl, $productPk, $dateStart, $dateEnd, $qty); + } + + // Debug Row + $rows[] = [ + 'year' => $year, + 'invoice_pk' => $invoicePk, + 'invoice_date'=> $invoiceDate, + 'chapter_id' => $chapterId ?? 0, + 'source' => $isFromJournal ? 'journal' : 'direct', + 'product_pk' => $productPk, + 'product_no' => $productNo, + 'title' => $title, + 'date_start' => $dateStart ?? '', + 'date_end' => $dateEnd ?? '', + 'qty' => $qty, + 'revenue_net' => $revenueNet, + ]; + + /* ===== Bundle-Auflösung ===== + - Bundle-Erkennung NUR über is_virtual (Fix!) + - Auslastung inkl Bundle: Komponenten bekommen qty*amount über Zeitraum + - Umsatz inkl Bundle: Umsatz wird proportional auf Leaf-Produkte verteilt + */ + $p = getProduct($Epi, $productPk); + $isBundle = (bool)($p->is_virtual ?? false); // ✅ FIX: nicht über materials ableiten + + if ($isBundle) { + $leafMap = resolveBundleLeafMap($Epi, $productPk); + + // Umsatz allokieren + if ($revenueNet != 0.0 && $year) { + $alloc = allocateBundleRevenue($Epi, $productPk, $revenueNet); + foreach ($alloc as $leafPk => $leafRevenue) { + $leafPk = (int)$leafPk; + ensureMeta($Epi, $leafPk, 0, ''); + addRevenue($pivotB, $leafPk, $year, (float)$leafRevenue); + } + } + + // Auslastung allokieren + if ($dateStart && $dateEnd && $qty > 0) { + foreach ($leafMap as $leafPk => $amt) { + $leafPk = (int)$leafPk; + $amt = (float)$amt; + if ($leafPk <= 0 || $amt <= 0) continue; + ensureMeta($Epi, $leafPk, 0, ''); + addIntervalEvent($eventsIncl, $leafPk, $dateStart, $dateEnd, $qty * $amt); + } + } + } +} + +/* ========================= + 1) Invoices holen + ========================= */ + +$invoiceAll = apiJsonDecode($Epi->requestEpiApi('/v1/invoice/all?ir=true&ib=true&cl=' . Epirent_Mandant)); +$invoiceList = ($invoiceAll && ($invoiceAll->success ?? false) && is_array($invoiceAll->payload ?? null)) ? $invoiceAll->payload : []; + +foreach ($invoiceList as $inv) { + $invoicePk = (int)($inv->primary_key ?? 0); + if ($invoicePk <= 0) continue; + + $invRes = apiJsonDecode($Epi->requestEpiApi('/v1/invoice/' . $invoicePk . '?cl=' . Epirent_Mandant)); + $invObj = ($invRes && ($invRes->success ?? false) && !empty($invRes->payload[0])) ? $invRes->payload[0] : null; + if (!$invObj) continue; + + $invoiceDate = (string)($invObj->invoice_date ?? ''); + $invoiceYear = yearFromDate($invoiceDate) ?? 0; + + $orderItems = $invObj->order_items ?? []; + if (!is_array($orderItems)) $orderItems = []; + + foreach ($orderItems as $oi) { + $oiType = (int)($oi->type ?? -1); + $oiPk = (int)($oi->primary_key ?? 0); + + // 1) Kapitel-Zeile (type=5): chid ist primary_key der Kapitelzeile! + if ($oiType === 5 && $oiPk > 0) { + $chapterId = $oiPk; + $journalItems = getJournalByChapter($Epi, $chapterId); + + foreach ($journalItems as $ji) { + processLineItem($Epi, $rows, $invoicePk, $invoiceDate, $invoiceYear, $chapterId, $ji, true); + } + continue; + } + + // 2) Direkte Artikel-Zeile ohne Kapitel (type=0) + if ($oiType === 0 && (int)($oi->product_pk ?? 0) > 0) { + processLineItem($Epi, $rows, $invoicePk, $invoiceDate, $invoiceYear, null, $oi, false); + continue; + } + + // Andere Typen ignorieren (Zwischensumme etc.) + } +} + +/* ========================= + 2) Years + Pivot Rows bauen + ========================= */ + +$years = array_keys($allYears); +sort($years); + +// Pivot Rows: pro Produkt => revenueDirect / revenueIncl und PeakDirect / PeakIncl + Histogramme +$pivotRows = []; + +foreach ($meta as $productPk => $m) { + $productPk = (int)$productPk; + + // revenue direct/incl + $sumDirect = 0.0; + $sumIncl = 0.0; + + $row = [ + 'product_pk' => $productPk, + 'product_no' => (int)($m['product_no'] ?? 0), + 'title' => (string)($m['title'] ?? ''), + 'years' => [], + 'total_direct'=> 0.0, + 'total_incl' => 0.0, + 'peak_direct' => 0, + 'peak_incl' => 0, + 'hist_direct' => [], + 'hist_incl' => [], + ]; + + foreach ($years as $y) { + $d = (float)($pivot[$productPk][$y] ?? 0.0); + $b = (float)($pivotB[$productPk][$y] ?? 0.0); + $incl = $d + $b; + + $row['years'][$y] = ['direct'=>$d, 'incl'=>$incl]; + + $sumDirect += $d; + $sumIncl += $incl; + } + + $row['total_direct'] = $sumDirect; + $row['total_incl'] = $sumIncl; + + // Peak + Histogram + $pd = computePeakAndHistogram($GLOBALS['eventsDirect'][$productPk] ?? []); + $pi = computePeakAndHistogram($GLOBALS['eventsIncl'][$productPk] ?? []); + + $row['peak_direct'] = (int)$pd['peak']; + $row['peak_incl'] = (int)$pi['peak']; + $row['hist_direct'] = $pd['hist']; + $row['hist_incl'] = $pi['hist']; + + // Nur Produkte zeigen, die überhaupt irgendwo vorkommen (direct oder incl) + if ($row['total_direct'] == 0.0 && $row['total_incl'] == 0.0 && $row['peak_direct'] == 0 && $row['peak_incl'] == 0) { + continue; + } + + $pivotRows[] = $row; +} + +// Sort: nach Total_incl absteigend (inkl Bundle-Anteil ist meist interessanter) +usort($pivotRows, fn($a,$b) => ($b['total_incl'] <=> $a['total_incl'])); + +/* ========================= + 3) Excel Export (nur Pivot) + ========================= */ + +$exportDir = __DIR__ . '/exports'; +if (!is_dir($exportDir)) { + @mkdir($exportDir, 0775, true); +} +$timestamp = date('Ymd_His'); +$excelFileName = "ROI_Artikel_Jahre_{$timestamp}.xlsx"; +$excelFilePath = $exportDir . '/' . $excelFileName; +$excelDownloadUrl = 'exports/' . $excelFileName; + +$spreadsheet = new Spreadsheet(); +$sheet = $spreadsheet->getActiveSheet(); +$sheet->setTitle('Pivot'); + +$cols = ['Produkt-PK','Artikel-Nr','Artikel','Peak','Peak (inkl. Bundle)','Summe','Summe (inkl. Bundle)']; +foreach ($years as $y) { + $cols[] = (string)$y; + $cols[] = (string)$y . ' (inkl. Bundle)'; +} + +$colCount = count($cols); +$lastColLetter = Coordinate::stringFromColumnIndex($colCount); + +// Header +$sheet->setCellValue('A1', 'EpiWebview – ROI / Umsatz pro Artikel & Jahr'); +$sheet->mergeCells('A1:' . $lastColLetter . '1'); +$sheet->getStyle('A1:' . $lastColLetter . '1')->getFont()->setBold(true)->setSize(14); +$sheet->getStyle('A1:' . $lastColLetter . '1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); + +$sheet->setCellValue('A2', 'Exportdatum: ' . date('d.m.Y H:i') . ' | Mandant: ' . (defined('Epirent_Mandant') ? Epirent_Mandant : '')); +$sheet->mergeCells('A2:' . $lastColLetter . '2'); +$sheet->getStyle('A2:' . $lastColLetter . '2')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); + +$headerRow = 4; +for ($i=0; $i<$colCount; $i++) { + $addr = Coordinate::stringFromColumnIndex($i+1) . $headerRow; + $sheet->setCellValue($addr, $cols[$i]); + $sheet->getStyle($addr)->getFont()->setBold(true); + $sheet->getStyle($addr)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); + $sheet->getColumnDimension(Coordinate::stringFromColumnIndex($i+1))->setAutoSize(true); +} + +$rowIdx = $headerRow + 1; +foreach ($pivotRows as $r) { + $c = 1; + $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c++).$rowIdx, (int)$r['product_pk'], DataType::TYPE_NUMERIC); + $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c++).$rowIdx, (int)$r['product_no'], DataType::TYPE_NUMERIC); + $sheet->setCellValue(Coordinate::stringFromColumnIndex($c++).$rowIdx, (string)$r['title']); + + $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c++).$rowIdx, (int)$r['peak_direct'], DataType::TYPE_NUMERIC); + $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c++).$rowIdx, (int)$r['peak_incl'], DataType::TYPE_NUMERIC); + + $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, (float)$r['total_direct'], DataType::TYPE_NUMERIC); + $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]'); + $c++; + + $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, (float)$r['total_incl'], DataType::TYPE_NUMERIC); + $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]'); + $c++; + + foreach ($years as $y) { + $d = (float)$r['years'][$y]['direct']; + $iVal = (float)$r['years'][$y]['incl']; + + $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, $d, DataType::TYPE_NUMERIC); + $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]'); + $c++; + + $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($c).$rowIdx, $iVal, DataType::TYPE_NUMERIC); + $sheet->getStyle(Coordinate::stringFromColumnIndex($c).$rowIdx)->getNumberFormat()->setFormatCode('#,##0.00 [$€-de-DE]'); + $c++; + } + + $rowIdx++; +} + +$sheet->setAutoFilter("A{$headerRow}:{$lastColLetter}{$headerRow}"); +$sheet->freezePane("A" . ($headerRow + 1)); + +// Save +$writer = new Xlsx($spreadsheet); +$writer->save($excelFilePath); + +/* ========================= + 4) HTML Ausgabe + ========================= */ +?> + + + + + + + ROI – Umsatz pro Artikel / Jahr + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+

ROI

+ + +
+
+
Pivot: Artikel × Rechnungsjahr
+ + Export nach Excel + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Produkt-PKArtikel-NrArtikelPeakSummeHistogramm
+ (' . $peakIncl . ')'; + ?> + + (' . formatEuro($sumIncl) . ')'; + ?> + + + () + + + + +
+ + Jahr-Zuordnung Umsatz: invoice_date (Rechnungsdatum). Peak/Histogramm: Zeitraum date_start..date_end. + Werte in Klammern = inkl. Bundle-Anteil. + +
+
+
+ +
+
+ Debug: verarbeitete Mietpositionen +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
JahrInvoiceRechnungsdatumQuelleChapterProdukt-PKArtikel-NrArtikelStartEndeMengeUmsatz netto
+ + Wenn ein Artikel “ohne Kapitel” fehlt, muss er hier als Quelle direct auftauchen. + +
+
+
+ +
+
+
+
+
+ + + + + + + + diff --git a/sources/getSidenav.php b/sources/getSidenav.php index e48be09..3a8d883 100644 --- a/sources/getSidenav.php +++ b/sources/getSidenav.php @@ -27,6 +27,10 @@ Warengruppencheck + + + Return-Of-Invest +
Addons