ROI: Stornierungen klammern jetzt die Zugehörige Buchung aus
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# EPIWebview
|
||||
|
||||
- **aktuellste stable Version:** 1.9.0
|
||||
- **aktuellste stable Version:** 1.10.2
|
||||
- **Lizenz:** Creative Commons Attribution-NonCommercial-ShareAlike 4.0 (CC BY-NC-SA 4.0). Einsehbar unter: (LICENSE.MD)
|
||||
- **Kompatibilität:** Erweiterung für **Epirent** und **CrewBrain**
|
||||
|
||||
@@ -17,7 +17,7 @@ Die Anwendung ist speziell für den Einsatz in Lagerprozessen entwickelt.
|
||||
|
||||
- **Check-In / Check-Out Übersicht**: Lagermonitor
|
||||
- **Integration mit Epirent API**: Vollständig kompatibel mit bestehenden Epirent-Systemen.
|
||||
- **Integration mit Crewbrain**: Anzeige einer Aufgabenliste aus CrewBraingit
|
||||
- **Integration mit Crewbrain**: Anzeige einer Aufgabenliste aus CrewBrain
|
||||
|
||||
---
|
||||
|
||||
@@ -26,7 +26,7 @@ Die Anwendung ist speziell für den Einsatz in Lagerprozessen entwickelt.
|
||||
## Systemanforderungen
|
||||
|
||||
- **Server:** PHP ≥ 8.2, Apache oder Nginx
|
||||
- Achtung !!: Für die Return of Invest Funktion oder den Warengruppencheck / Imagechecks sollten in der php.ini die max_execution_time und die maximale Dateigröße deutlich nach oben korrigiert werden. Die ROI Funktion kann gut und gerne 20 Minuten laden!
|
||||
- Achtung !!: Für die Return of Invest Funktion oder den Warengruppencheck / Imagechecks sollten in der php.ini die max_execution_time und die maximale Dateigröße deutlich nach oben korrigiert werden. Die ROI Funktion kann gut und gerne 20 Minuten laden!. Foglende PHP Funktionen müssen aktiviert werden: curl, gzcompress, Imagick, gd
|
||||
- **Client:** Aktueller Browser (Chrome, Edge, Firefox, Safari)
|
||||
- **Datenquelle:** Bestehende Epirent-Installation mit aktivierter API sowie optional CrewBrain
|
||||
|
||||
|
||||
382
dist/ROI.php
vendored
382
dist/ROI.php
vendored
@@ -94,12 +94,7 @@ function getRentPrice(Epirent $Epi, int $productPk): float {
|
||||
|
||||
/**
|
||||
* WICHTIG: Bundle-Auflösung NUR für virtuelle Bundles.
|
||||
* Damit ist die anteilige Bundlepreis-Berechnung wieder wie in der funktionierenden Version.
|
||||
*
|
||||
* Ergebnis: [leafProductPk => amount]
|
||||
* - nur wenn is_virtual == true UND rent_fix/materials vorhanden
|
||||
* - sonst: leaf = [self=>1]
|
||||
* - Cycle-Guard über $stack
|
||||
*/
|
||||
function resolveBundleLeafMap(Epirent $Epi, int $productPk, array &$stack = []): array {
|
||||
global $bundleLeafCache;
|
||||
@@ -222,11 +217,10 @@ function getJournalByChapter(Epirent $Epi, int $chapterId): array {
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Interval aggregation (Peak/Histogram) – tagesbasiert (wie gehabt)
|
||||
Interval aggregation (Peak/Histogram) – tagesbasiert
|
||||
========================= */
|
||||
|
||||
// eventsDirect[productPk][ymd] += deltaQty
|
||||
$eventsDirect = [];
|
||||
$eventsDirect = []; // eventsDirect[productPk][ymd] += deltaQty
|
||||
$eventsIncl = []; // direct + bundle
|
||||
|
||||
function addIntervalEvent(array &$events, int $productPk, string $startYmd, string $endYmd, float $qty): void {
|
||||
@@ -264,6 +258,8 @@ function computePeakAndHistogram(array $eventMap): array {
|
||||
for ($i = 0; $i < count($dates); $i++) {
|
||||
$d = $dates[$i];
|
||||
$level += (float)$eventMap[$d];
|
||||
|
||||
// clamp, damit kein negativer Bestand "Tage" erzeugt
|
||||
if ($level < 0) $level = 0;
|
||||
|
||||
$intLevel = (int)round($level);
|
||||
@@ -300,10 +296,6 @@ $meta = []; // product meta [pk=>['product_no'=>..,'title'=>..]]
|
||||
$allYears = [];
|
||||
|
||||
// Auftragsbasierte "Slots" (direct/incl) pro Produkt
|
||||
// slotAgg[productPk]['direct'][slot] += revenue
|
||||
// slotAgg[productPk]['incl'][slot] += revenue
|
||||
// slotAgg[productPk]['direct_ext'][slot] += extRevenue
|
||||
// slotAgg[productPk]['incl_ext'][slot] += extRevenue
|
||||
$slotAgg = [];
|
||||
|
||||
function ensureMeta(Epirent $Epi, int $productPk, int $productNo, string $title): void {
|
||||
@@ -329,7 +321,6 @@ function addRevenue(array &$pivotRef, int $productPk, int $year, float $revenueN
|
||||
* Extern-Logik:
|
||||
* - Amount External ist eine ANZAHL (nicht Umsatz).
|
||||
* - Wir berechnen extRevenue = sum_total_net * (amount_external / amount_total) (wenn amount_total>0)
|
||||
* - fallback: 0
|
||||
*/
|
||||
function calcExternalRevenueNet(object $li): float {
|
||||
$amountTotal = (float)($li->amount_total ?? 0);
|
||||
@@ -346,103 +337,219 @@ function calcExternalRevenueNet(object $li): float {
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Slot (auftragsbasiert) – Intervall Coloring
|
||||
Slot (auftragsbasiert) – Storno-sicher (SIGNED)
|
||||
========================= */
|
||||
|
||||
/**
|
||||
* Intervall-Item für Slotting (ein "Auftragsteil" pro Produkt)
|
||||
* qty kann positiv oder negativ sein (negativ = Storno/Rückbuchung)
|
||||
*/
|
||||
function addSlotItem(array &$slotItems, int $productPk, int $invoicePk, int $orderNo, string $startYmd, string $endYmd, float $qty, float $revenueNet, float $extRevenueNet): void {
|
||||
function addSlotItem(
|
||||
array &$slotItems,
|
||||
int $productPk,
|
||||
int $invoicePk,
|
||||
int $orderPk,
|
||||
int $invoiceNo,
|
||||
string $startYmd,
|
||||
string $endYmd,
|
||||
float $qty,
|
||||
float $revenueNet,
|
||||
float $extRevenueNet
|
||||
): void {
|
||||
$startYmd = safeYmd($startYmd);
|
||||
$endYmd = safeYmd($endYmd);
|
||||
if ($productPk <= 0) return;
|
||||
if (!$startYmd || !$endYmd) return;
|
||||
if ($qty <= 0) return;
|
||||
if ($qty == 0.0) return;
|
||||
|
||||
if (!isset($slotItems[$productPk])) $slotItems[$productPk] = [];
|
||||
|
||||
$slotItems[$productPk][] = [
|
||||
'invoice_pk' => $invoicePk,
|
||||
'order_no' => $orderNo,
|
||||
'start_ts' => ymdToTs($startYmd),
|
||||
'end_ts' => ymdToTs($endYmd),
|
||||
'qty' => (float)$qty,
|
||||
'rev' => (float)$revenueNet,
|
||||
'rev_ext' => (float)$extRevenueNet,
|
||||
'start_ymd' => $startYmd,
|
||||
'end_ymd' => $endYmd,
|
||||
'product_pk' => $productPk,
|
||||
'invoice_pk' => $invoicePk,
|
||||
'order_pk' => $orderPk,
|
||||
'invoice_no' => $invoiceNo,
|
||||
'start_ts' => ymdToTs($startYmd),
|
||||
'end_ts' => ymdToTs($endYmd),
|
||||
'qty' => (float)$qty, // signed erlaubt
|
||||
'rev' => (float)$revenueNet,
|
||||
'rev_ext' => (float)$extRevenueNet,
|
||||
'start_ymd' => $startYmd,
|
||||
'end_ymd' => $endYmd,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Slotting-Regel:
|
||||
* - Aufträge werden nach start_ts sortiert,
|
||||
* - bei gleichem start: zuerst der der später endet bekommt den "zweiten" Slot (also: end_ts DESC),
|
||||
* - wenn dann noch gleich: nach order_no (oder invoice_pk) ASC.
|
||||
* - Belegung: für qty=2 werden 2 Slots gesucht; wenn Slots frei werden (end < start), wiederverwendbar.
|
||||
* Slotting (SIGNED, Storno gibt Slots wieder frei):
|
||||
* - Positive qty belegt Slots.
|
||||
* - Negative qty bucht Umsatz zurück UND gibt Slots frei.
|
||||
* - Priorität bei Storno: zuerst Slots mit gleichem Zeitraum (gleicher release_ts) abbauen.
|
||||
*
|
||||
* Umsatzverteilung:
|
||||
* - pro Auftrag & Produkt wird der UMSATZ auf die belegten Slots gleichmäßig verteilt (rev/qty)
|
||||
* - analog für extern-umsatz.
|
||||
* Ergebnis:
|
||||
* - 'direct' => [slot => revenue]
|
||||
* - 'direct_ext' => [slot => extRevenue]
|
||||
*/
|
||||
function computeOrderBasedSlots(array $itemsForProduct): array {
|
||||
if (empty($itemsForProduct)) return [
|
||||
'direct' => [],
|
||||
'direct_ext' => []
|
||||
];
|
||||
if (empty($itemsForProduct)) {
|
||||
return ['direct' => [], 'direct_ext' => []];
|
||||
}
|
||||
|
||||
// Sortierung: start ASC, end DESC, invoice_no ASC, invoice_pk ASC
|
||||
usort($itemsForProduct, function($a, $b){
|
||||
if ($a['start_ts'] !== $b['start_ts']) return $a['start_ts'] <=> $b['start_ts'];
|
||||
if ($a['end_ts'] !== $b['end_ts']) return $b['end_ts'] <=> $a['end_ts']; // später endend zuerst
|
||||
if ($a['order_no'] !== $b['order_no']) return $a['order_no'] <=> $b['order_no'];
|
||||
return $a['invoice_pk'] <=> $b['invoice_pk'];
|
||||
if (($a['start_ts'] ?? 0) !== ($b['start_ts'] ?? 0)) return ($a['start_ts'] ?? 0) <=> ($b['start_ts'] ?? 0);
|
||||
if (($a['end_ts'] ?? 0) !== ($b['end_ts'] ?? 0)) return ($b['end_ts'] ?? 0) <=> ($a['end_ts'] ?? 0);
|
||||
if (($a['invoice_no'] ?? 0) !== ($b['invoice_no'] ?? 0)) return ($a['invoice_no'] ?? 0) <=> ($b['invoice_no'] ?? 0);
|
||||
return ($a['invoice_pk'] ?? 0) <=> ($b['invoice_pk'] ?? 0);
|
||||
});
|
||||
|
||||
$slotEnd = []; // slotIndex => end_ts
|
||||
$slotRevenue = []; // slotIndex => revenue
|
||||
$slotRevenueExt = []; // slotIndex => revenueExt
|
||||
// Active slots: slotIndex => release_ts
|
||||
$active = [];
|
||||
// release_ts => [slotIndex, ...]
|
||||
$activeByRelease = [];
|
||||
// free slot indices
|
||||
$freeSlots = [];
|
||||
|
||||
// Revenues per slot
|
||||
$slotRevenue = [];
|
||||
$slotRevenueExt = [];
|
||||
|
||||
$releaseTsFor = function(int $endTs): int {
|
||||
// Ende inklusiv -> frei am Folgetag 00:00
|
||||
return $endTs + 86400;
|
||||
};
|
||||
|
||||
$freeExpired = function(int $currentStartTs) use (&$active, &$activeByRelease, &$freeSlots) {
|
||||
if (empty($active)) return;
|
||||
foreach ($active as $slot => $relTs) {
|
||||
if ($relTs <= $currentStartTs) {
|
||||
unset($active[$slot]);
|
||||
if (isset($activeByRelease[$relTs])) {
|
||||
$activeByRelease[$relTs] = array_values(array_filter(
|
||||
$activeByRelease[$relTs],
|
||||
fn($s) => (int)$s !== (int)$slot
|
||||
));
|
||||
if (empty($activeByRelease[$relTs])) unset($activeByRelease[$relTs]);
|
||||
}
|
||||
$freeSlots[] = (int)$slot;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$takeFreeSlot = function() use (&$freeSlots, &$active): int {
|
||||
if (!empty($freeSlots)) {
|
||||
sort($freeSlots);
|
||||
return (int)array_shift($freeSlots);
|
||||
}
|
||||
if (empty($active)) return 1;
|
||||
$keys = array_keys($active);
|
||||
return (int)(max($keys) + 1);
|
||||
};
|
||||
|
||||
foreach ($itemsForProduct as $it) {
|
||||
$qty = (int)round($it['qty']);
|
||||
if ($qty <= 0) continue;
|
||||
$startTs = (int)($it['start_ts'] ?? 0);
|
||||
$endTs = (int)($it['end_ts'] ?? 0);
|
||||
if ($startTs <= 0 || $endTs <= 0) continue;
|
||||
if ($endTs < $startTs) continue;
|
||||
|
||||
$revPer = ($it['rev'] ?? 0.0) / $qty;
|
||||
$extPer = ($it['rev_ext'] ?? 0.0) / $qty;
|
||||
// vor jeder Aktion: abgelaufene Slots freigeben
|
||||
$freeExpired($startTs);
|
||||
|
||||
// finde freie Slots / neue Slots
|
||||
$assigned = [];
|
||||
for ($k=0; $k<$qty; $k++) {
|
||||
$slot = null;
|
||||
$qtySigned = (float)($it['qty'] ?? 0.0);
|
||||
if ($qtySigned == 0.0) continue;
|
||||
|
||||
// freier Slot: end < start
|
||||
foreach ($slotEnd as $idx => $endTs) {
|
||||
if ($endTs < $it['start_ts']) { // streng <, damit gleicher Tag als parallel zählt
|
||||
$slot = (int)$idx;
|
||||
break;
|
||||
$qtyAbs = (int)round(abs($qtySigned));
|
||||
if ($qtyAbs <= 0) continue;
|
||||
|
||||
$rev = (float)($it['rev'] ?? 0.0);
|
||||
$ext = (float)($it['rev_ext'] ?? 0.0);
|
||||
|
||||
$revPer = $rev / $qtyAbs;
|
||||
$extPer = $ext / $qtyAbs;
|
||||
|
||||
$relTs = $releaseTsFor($endTs);
|
||||
|
||||
if ($qtySigned > 0) {
|
||||
// --- Belegen ---
|
||||
for ($k = 0; $k < $qtyAbs; $k++) {
|
||||
$slot = $takeFreeSlot();
|
||||
|
||||
$active[$slot] = $relTs;
|
||||
if (!isset($activeByRelease[$relTs])) $activeByRelease[$relTs] = [];
|
||||
$activeByRelease[$relTs][] = $slot;
|
||||
|
||||
if (!isset($slotRevenue[$slot])) $slotRevenue[$slot] = 0.0;
|
||||
if (!isset($slotRevenueExt[$slot])) $slotRevenueExt[$slot] = 0.0;
|
||||
|
||||
$slotRevenue[$slot] += $revPer;
|
||||
$slotRevenueExt[$slot] += $extPer;
|
||||
}
|
||||
} else {
|
||||
// --- Storno / Freigeben ---
|
||||
// erst gleiche release_ts abbauen (damit "Rechnung+Storno im gleichen Zeitraum" sauber 0 Slots macht)
|
||||
$candidates = $activeByRelease[$relTs] ?? [];
|
||||
|
||||
for ($k = 0; $k < $qtyAbs; $k++) {
|
||||
$slot = null;
|
||||
|
||||
if (!empty($candidates)) {
|
||||
rsort($candidates); // höchste Slotnummer zuerst abbauen
|
||||
$slot = (int)array_shift($candidates);
|
||||
} else {
|
||||
if (!empty($active)) {
|
||||
$keys = array_keys($active);
|
||||
rsort($keys);
|
||||
$slot = (int)$keys[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($slot === null) {
|
||||
$slot = count($slotEnd) + 1; // Slots sind 1-based
|
||||
}
|
||||
|
||||
// reservieren
|
||||
$slotEnd[$slot] = $it['end_ts'];
|
||||
$assigned[] = $slot;
|
||||
if ($slot === null) break;
|
||||
|
||||
// Revenue sammeln
|
||||
if (!isset($slotRevenue[$slot])) $slotRevenue[$slot] = 0.0;
|
||||
if (!isset($slotRevenueExt[$slot])) $slotRevenueExt[$slot] = 0.0;
|
||||
$slotRevenue[$slot] += $revPer;
|
||||
$slotRevenueExt[$slot] += $extPer;
|
||||
// Umsatz auf Slot zurückbuchen
|
||||
if (!isset($slotRevenue[$slot])) $slotRevenue[$slot] = 0.0;
|
||||
if (!isset($slotRevenueExt[$slot])) $slotRevenueExt[$slot] = 0.0;
|
||||
$slotRevenue[$slot] += $revPer; // revPer ist i.d.R. negativ
|
||||
$slotRevenueExt[$slot] += $extPer; // extPer ist i.d.R. negativ
|
||||
|
||||
// Slot freigeben
|
||||
$oldRel = $active[$slot] ?? null;
|
||||
unset($active[$slot]);
|
||||
|
||||
if ($oldRel !== null && isset($activeByRelease[$oldRel])) {
|
||||
$activeByRelease[$oldRel] = array_values(array_filter(
|
||||
$activeByRelease[$oldRel],
|
||||
fn($s) => (int)$s !== (int)$slot
|
||||
));
|
||||
if (empty($activeByRelease[$oldRel])) unset($activeByRelease[$oldRel]);
|
||||
}
|
||||
|
||||
$freeSlots[] = $slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ksort($slotRevenue);
|
||||
ksort($slotRevenueExt);
|
||||
// --- Cleanup: Slots ohne Umsatz entfernen + neu durchnummerieren ---
|
||||
$eps = 0.00001;
|
||||
$allSlots = array_unique(array_merge(array_keys($slotRevenue), array_keys($slotRevenueExt)));
|
||||
sort($allSlots);
|
||||
|
||||
// runden (2 Nachkommastellen für Anzeige später, intern float ok)
|
||||
return [
|
||||
'direct' => $slotRevenue,
|
||||
'direct_ext' => $slotRevenueExt
|
||||
];
|
||||
$kept = [];
|
||||
foreach ($allSlots as $s) {
|
||||
$rev = (float)($slotRevenue[$s] ?? 0.0);
|
||||
$ext = (float)($slotRevenueExt[$s] ?? 0.0);
|
||||
if (abs($rev) < $eps && abs($ext) < $eps) continue;
|
||||
$kept[] = $s;
|
||||
}
|
||||
|
||||
$newRev = [];
|
||||
$newExt = [];
|
||||
$idx = 1;
|
||||
foreach ($kept as $old) {
|
||||
$newRev[$idx] = (float)($slotRevenue[$old] ?? 0.0);
|
||||
$newExt[$idx] = (float)($slotRevenueExt[$old] ?? 0.0);
|
||||
$idx++;
|
||||
}
|
||||
|
||||
return ['direct' => $newRev, 'direct_ext' => $newExt];
|
||||
}
|
||||
|
||||
/* =========================
|
||||
@@ -454,13 +561,15 @@ function processLineItem(
|
||||
array &$rows,
|
||||
array &$slotItemsDirect,
|
||||
array &$slotItemsIncl,
|
||||
int $orderPk,
|
||||
int $invoicePk,
|
||||
int $invoiceNo,
|
||||
string $invoiceDate,
|
||||
int $invoiceYear,
|
||||
?int $chapterId,
|
||||
object $li,
|
||||
bool $isFromJournal
|
||||
bool $isFromJournal,
|
||||
bool $invoiceIsCredit // <-- wichtig: Storno-Rechnung (z.B. sum_net < 0)
|
||||
): void {
|
||||
global $pivot, $pivotB, $pivotExt, $pivotExtB, $allYears, $eventsDirect, $eventsIncl;
|
||||
|
||||
@@ -471,15 +580,23 @@ function processLineItem(
|
||||
$title = (string)($li->title ?? '');
|
||||
$productNo = (int)($li->product_no ?? 0);
|
||||
|
||||
$qty = (float)($li->amount_total ?? 0);
|
||||
if ($qty <= 0) $qty = 0.0;
|
||||
$qtyRaw = (float)($li->amount_total ?? 0);
|
||||
if ($qtyRaw == 0.0) $qtyRaw = 0.0;
|
||||
|
||||
// Umsatz bleibt wie geliefert (kann negativ sein)
|
||||
$revenueNet = (float)($li->sum_total_net ?? 0);
|
||||
$extRevenueNet = calcExternalRevenueNet($li);
|
||||
|
||||
// Für Auslastung/Slots: wenn Storno-Rechnung, Menge "negativ signieren"
|
||||
// (weil Epi bei Storno typischerweise Menge positiv lässt, aber Preise negativ macht)
|
||||
$qtySigned = $qtyRaw;
|
||||
if ($invoiceIsCredit && $qtySigned != 0.0) {
|
||||
$qtySigned = -abs($qtySigned);
|
||||
}
|
||||
|
||||
$dateStart = safeYmd((string)($li->date_start ?? '')) ?: null;
|
||||
$dateEnd = safeYmd((string)($li->date_end ?? '')) ?: null;
|
||||
|
||||
$revenueNet = (float)($li->sum_total_net ?? 0);
|
||||
$extRevenueNet = calcExternalRevenueNet($li);
|
||||
|
||||
ensureMeta($Epi, $productPk, $productNo, $title);
|
||||
|
||||
// Jahr: Rechnungsdatum
|
||||
@@ -493,15 +610,16 @@ function processLineItem(
|
||||
if ($extRevenueNet != 0.0 && $year) addRevenue($pivotExt, $productPk, $year, $extRevenueNet);
|
||||
|
||||
// Peak/Histogram direct + incl (tagesbasiert)
|
||||
if ($dateStart && $dateEnd && $qty > 0) {
|
||||
addIntervalEvent($eventsDirect, $productPk, $dateStart, $dateEnd, $qty);
|
||||
addIntervalEvent($eventsIncl, $productPk, $dateStart, $dateEnd, $qty);
|
||||
// -> qtySigned sorgt dafür, dass Storno zeitgleich aufhebt
|
||||
if ($dateStart && $dateEnd && $qtySigned != 0.0) {
|
||||
addIntervalEvent($eventsDirect, $productPk, $dateStart, $dateEnd, $qtySigned);
|
||||
addIntervalEvent($eventsIncl, $productPk, $dateStart, $dateEnd, $qtySigned);
|
||||
}
|
||||
|
||||
// Slotting: direct
|
||||
if ($dateStart && $dateEnd && $qty > 0) {
|
||||
addSlotItem($slotItemsDirect, $productPk, $invoicePk, $invoiceNo, $dateStart, $dateEnd, $qty, $revenueNet, $extRevenueNet);
|
||||
addSlotItem($slotItemsIncl, $productPk, $invoicePk, $invoiceNo, $dateStart, $dateEnd, $qty, $revenueNet, $extRevenueNet);
|
||||
// Slotting: direct (signed qty)
|
||||
if ($dateStart && $dateEnd && $qtySigned != 0.0) {
|
||||
addSlotItem($slotItemsDirect, $productPk, $invoicePk, $orderPk, $invoiceNo, $dateStart, $dateEnd, $qtySigned, $revenueNet, $extRevenueNet);
|
||||
addSlotItem($slotItemsIncl, $productPk, $invoicePk, $orderPk, $invoiceNo, $dateStart, $dateEnd, $qtySigned, $revenueNet, $extRevenueNet);
|
||||
}
|
||||
|
||||
// Debug Row
|
||||
@@ -517,11 +635,13 @@ function processLineItem(
|
||||
'title' => $title,
|
||||
'date_start' => $dateStart ?? '',
|
||||
'date_end' => $dateEnd ?? '',
|
||||
'qty' => $qty,
|
||||
'qty' => $qtyRaw,
|
||||
'qty_signed' => $qtySigned,
|
||||
'revenue_net' => $revenueNet,
|
||||
'ext_rev_net' => $extRevenueNet,
|
||||
'amount_ext' => (float)($li->amount_external ?? 0),
|
||||
'amount_total'=> (float)($li->amount_total ?? 0),
|
||||
'is_credit' => $invoiceIsCredit ? 1 : 0,
|
||||
];
|
||||
|
||||
/* ===== Bundle-Auflösung (nur virtuelle Bundles!) ===== */
|
||||
@@ -549,20 +669,19 @@ function processLineItem(
|
||||
}
|
||||
}
|
||||
|
||||
// Auslastung allokieren (Peak/Histogramm inkl Bundle)
|
||||
if ($dateStart && $dateEnd && $qty > 0) {
|
||||
// Auslastung allokieren (Peak/Histogramm inkl Bundle) – signed qty!
|
||||
if ($dateStart && $dateEnd && $qtySigned != 0.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);
|
||||
addIntervalEvent($eventsIncl, $leafPk, $dateStart, $dateEnd, $qtySigned * $amt);
|
||||
}
|
||||
}
|
||||
|
||||
// Slotting allokieren (inkl Bundle): wir erzeugen SlotItems für Leaf-Produkte
|
||||
if ($dateStart && $dateEnd && $qty > 0) {
|
||||
// Revenue pro "Bundle-Item" wird in allocateBundleRevenue bereits verteilt.
|
||||
// Slotting allokieren (inkl Bundle) – signed qty!
|
||||
if ($dateStart && $dateEnd && $qtySigned != 0.0) {
|
||||
$alloc = ($revenueNet != 0.0) ? allocateBundleRevenue($Epi, $productPk, $revenueNet) : [];
|
||||
$allocExt = ($extRevenueNet != 0.0) ? allocateBundleRevenue($Epi, $productPk, $extRevenueNet) : [];
|
||||
|
||||
@@ -574,11 +693,10 @@ function processLineItem(
|
||||
$leafRev = (float)($alloc[$leafPk] ?? 0.0);
|
||||
$leafExt = (float)($allocExt[$leafPk] ?? 0.0);
|
||||
|
||||
// qty_leaf = qty * amt
|
||||
$qtyLeaf = $qty * $amt;
|
||||
$qtyLeafSigned = $qtySigned * $amt;
|
||||
|
||||
ensureMeta($Epi, $leafPk, 0, '');
|
||||
addSlotItem($slotItemsIncl, $leafPk, $invoicePk, $invoiceNo, $dateStart, $dateEnd, $qtyLeaf, $leafRev, $leafExt);
|
||||
addSlotItem($slotItemsIncl, $leafPk, $invoicePk, $orderPk, $invoiceNo, $dateStart, $dateEnd, $qtyLeafSigned, $leafRev, $leafExt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -605,6 +723,10 @@ foreach ($invoiceList as $inv) {
|
||||
$invoiceDate = (string)($invObj->invoice_date ?? '');
|
||||
$invoiceYear = yearFromDate($invoiceDate) ?? 0;
|
||||
$invoiceNo = (int)($invObj->invoice_no ?? 0);
|
||||
$orderPk = (int)($invObj->order_pk ?? 0);
|
||||
|
||||
// Storno-Erkennung (Credit Note): sum_net < 0
|
||||
$invoiceIsCredit = ((float)($invObj->sum_net ?? 0.0)) < 0.0;
|
||||
|
||||
$orderItems = $invObj->order_items ?? [];
|
||||
if (!is_array($orderItems)) $orderItems = [];
|
||||
@@ -619,14 +741,14 @@ foreach ($invoiceList as $inv) {
|
||||
$journalItems = getJournalByChapter($Epi, $chapterId);
|
||||
|
||||
foreach ($journalItems as $ji) {
|
||||
processLineItem($Epi, $rows, $slotItemsDirect, $slotItemsIncl, $invoicePk, $invoiceNo, $invoiceDate, $invoiceYear, $chapterId, $ji, true);
|
||||
processLineItem($Epi, $rows, $slotItemsDirect, $slotItemsIncl, $orderPk, $invoicePk, $invoiceNo, $invoiceDate, $invoiceYear, $chapterId, $ji, true, $invoiceIsCredit);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Direkte Artikelposition ohne Kapitel (type=0)
|
||||
if ($oiType === 0 && (int)($oi->product_pk ?? 0) > 0) {
|
||||
processLineItem($Epi, $rows, $slotItemsDirect, $slotItemsIncl, $invoicePk, $invoiceNo, $invoiceDate, $invoiceYear, null, $oi, false);
|
||||
processLineItem($Epi, $rows, $slotItemsDirect, $slotItemsIncl, $orderPk, $invoicePk, $invoiceNo, $invoiceDate, $invoiceYear, null, $oi, false, $invoiceIsCredit);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -717,7 +839,6 @@ foreach ($meta as $productPk => $m) {
|
||||
$row['hist_direct'] = $pd['hist'];
|
||||
$row['hist_incl'] = $pi['hist'];
|
||||
|
||||
// nur anzeigen, wenn was los ist
|
||||
$hasAny = (
|
||||
$row['total_direct'] != 0.0 ||
|
||||
$row['total_incl'] != 0.0 ||
|
||||
@@ -850,13 +971,14 @@ $writer->save($excelFilePath);
|
||||
========================= */
|
||||
|
||||
function slotsToDisplay(array $slotMap): array {
|
||||
// slotMap: [slot=>revenue]
|
||||
ksort($slotMap);
|
||||
$out = [];
|
||||
foreach ($slotMap as $slot => $rev) {
|
||||
$slot = (int)$slot;
|
||||
if ($slot <= 0) continue;
|
||||
$out[] = ['slot'=>$slot, 'rev'=>(float)$rev];
|
||||
$v = (float)$rev;
|
||||
if (abs($v) < 0.0000001) $v = 0.0;
|
||||
$out[] = ['slot'=>$slot, 'rev'=>$v];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
@@ -884,11 +1006,9 @@ function slotsToDisplay(array $slotMap): array {
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" crossorigin="anonymous"></script>
|
||||
|
||||
<style>
|
||||
.kpi-updated { font-size: .82rem; opacity: .85; }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||
.btn-xs { padding:.15rem .4rem; font-size:.78rem; }
|
||||
.nowrap { white-space: nowrap; }
|
||||
.small-muted { font-size: .82rem; opacity:.85; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -974,37 +1094,28 @@ function slotsToDisplay(array $slotMap): array {
|
||||
<td><?php echo htmlspecialchars((string)$r['title']); ?></td>
|
||||
|
||||
<td class="nowrap">
|
||||
<?php
|
||||
echo $peakDirect;
|
||||
echo ' <span class="text-muted">(' . $peakIncl . ')</span>';
|
||||
?>
|
||||
<?php echo $peakDirect; ?>
|
||||
<span class="text-muted">(<?php echo $peakIncl; ?>)</span>
|
||||
</td>
|
||||
|
||||
<td class="nowrap">
|
||||
<?php
|
||||
echo formatEuro($sumDirect);
|
||||
echo ' <span class="text-muted">(' . formatEuro($sumIncl) . ')</span>';
|
||||
?>
|
||||
<?php echo formatEuro($sumDirect); ?>
|
||||
<span class="text-muted">(<?php echo formatEuro($sumIncl); ?>)</span>
|
||||
</td>
|
||||
|
||||
<td class="nowrap">
|
||||
<?php
|
||||
echo formatEuro($extDirect);
|
||||
echo ' <span class="text-muted">(' . formatEuro($extIncl) . ')</span>';
|
||||
?>
|
||||
<?php echo formatEuro($extDirect); ?>
|
||||
<span class="text-muted">(<?php echo formatEuro($extIncl); ?>)</span>
|
||||
</td>
|
||||
|
||||
<?php foreach ($years as $y): ?>
|
||||
<?php
|
||||
$d = (float)$r['years'][$y]['direct'];
|
||||
$iVal = (float)$r['years'][$y]['incl'];
|
||||
|
||||
$de = (float)$r['years_ext'][$y]['direct'];
|
||||
$ie = (float)$r['years_ext'][$y]['incl'];
|
||||
?>
|
||||
<td class="nowrap">
|
||||
<div><?php echo formatEuro($d); ?> <span class="text-muted">(<?php echo formatEuro($iVal); ?>)</span></div>
|
||||
<!--<div class="small-muted">ext: <?php echo formatEuro($de); ?> <span class="text-muted">(<?php echo formatEuro($ie); ?>)</span></div>-->
|
||||
<?php echo formatEuro($d); ?>
|
||||
<span class="text-muted">(<?php echo formatEuro($iVal); ?>)</span>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -1024,7 +1135,7 @@ function slotsToDisplay(array $slotMap): array {
|
||||
data-hist="<?php echo htmlspecialchars($histInclB64); ?>"
|
||||
data-peak="<?php echo (int)$peakIncl; ?>"
|
||||
data-mode="incl">
|
||||
Histogramm inkl. Bundle
|
||||
inkl. Bundle
|
||||
</button>
|
||||
</td>
|
||||
|
||||
@@ -1033,7 +1144,7 @@ function slotsToDisplay(array $slotMap): array {
|
||||
class="btn btn-outline-dark btn-xs js-slots"
|
||||
data-title="<?php echo htmlspecialchars((string)$r['title']); ?>"
|
||||
data-slots="<?php echo htmlspecialchars($slotsB64); ?>">
|
||||
Slots (Auftragsbasiert)
|
||||
Slots
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1045,8 +1156,9 @@ function slotsToDisplay(array $slotMap): array {
|
||||
Jahr-Zuordnung Umsatz: <span class="mono">invoice_date</span>.
|
||||
Peak/Histogramm: Zeitraum <span class="mono">date_start..date_end</span> (tagesbasiert).
|
||||
Werte in Klammern = inkl. Bundle-Anteil (nur <b>virtuelle</b> Bundles werden zerlegt).
|
||||
Extern-Umsatz: <span class="mono">sum_total_net * (amount_external/amount_total)</span> (amount_external ist Menge).
|
||||
Slots: auftragsbasierte Slotbelegung nach Start, dann Ende DESC, dann Auftragsnr.
|
||||
Extern-Umsatz: <span class="mono">sum_total_net * (amount_external/amount_total)</span>.
|
||||
Slots: auftragsbasiert, SIGNED (Storno gibt Slots frei und bucht auf die gleichen Zeiträume zurück).
|
||||
Storno-Erkennung: Rechnung mit <span class="mono">sum_net < 0</span>.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1073,9 +1185,11 @@ function slotsToDisplay(array $slotMap): array {
|
||||
<th>Start</th>
|
||||
<th>Ende</th>
|
||||
<th>Menge</th>
|
||||
<th>Menge (signed)</th>
|
||||
<th>Ext-Menge</th>
|
||||
<th>Umsatz netto</th>
|
||||
<th>Umsatz extern</th>
|
||||
<th>Credit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -1093,16 +1207,18 @@ function slotsToDisplay(array $slotMap): array {
|
||||
<td class="mono"><?php echo htmlspecialchars((string)$r['date_start']); ?></td>
|
||||
<td class="mono"><?php echo htmlspecialchars((string)$r['date_end']); ?></td>
|
||||
<td class="mono"><?php echo htmlspecialchars((string)$r['qty']); ?></td>
|
||||
<td class="mono"><?php echo htmlspecialchars((string)($r['qty_signed'] ?? $r['qty'])); ?></td>
|
||||
<td class="mono"><?php echo htmlspecialchars((string)($r['amount_ext'] ?? '0')); ?></td>
|
||||
<td class="nowrap"><?php echo formatEuro((float)$r['revenue_net']); ?></td>
|
||||
<td class="nowrap"><?php echo formatEuro((float)$r['ext_rev_net']); ?></td>
|
||||
<td class="mono"><?php echo (int)($r['is_credit'] ?? 0); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<small class="text-muted">
|
||||
Wenn ein Artikel “ohne Kapitel” fehlt, muss er hier als Quelle <b>direct</b> auftauchen.
|
||||
Ext-Menge ist Anzahl; Umsatz extern ist anteilig berechnet.
|
||||
Menge(signed) ist für Peak/Slots relevant (Storno-Rechnung -> negativ).
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1127,7 +1243,7 @@ function slotsToDisplay(array $slotMap): array {
|
||||
<div class="modal-body">
|
||||
<canvas id="histChart" height="140"></canvas>
|
||||
<div class="mt-2 text-muted">
|
||||
X-Achse: gleichzeitig vermietete Stückzahl (1..Peak) | Y-Achse: Tage
|
||||
X-Achse: gleichzeitig vermietete Stückzahl (1..Peak) | Y-Achse: Tage
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1148,6 +1264,7 @@ function slotsToDisplay(array $slotMap): array {
|
||||
<div class="mb-2 text-muted">
|
||||
Slot-Umsatz ist <b>auftragsbasiert</b>: Umsatz einer Position wird gleichmäßig auf die belegten Slots verteilt.
|
||||
Werte in Klammern = inkl. Bundle-Anteil (nur virtuelle Bundles).
|
||||
Storno bucht Umsatz auf die gleichen Slots zurück und gibt die Slots frei (SIGNED).
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
@@ -1228,7 +1345,6 @@ function openSlots(title, payload) {
|
||||
const directExt = payload.direct_ext || [];
|
||||
const inclExt = payload.incl_ext || [];
|
||||
|
||||
// maps slot->rev
|
||||
const mapD = {};
|
||||
const mapI = {};
|
||||
const mapDE = {};
|
||||
@@ -1289,9 +1405,7 @@ function openSlots(title, payload) {
|
||||
options: {
|
||||
responsive: true,
|
||||
animation: false,
|
||||
scales: {
|
||||
y: { beginAtZero: true }
|
||||
}
|
||||
scales: { y: { beginAtZero: true } }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1321,4 +1435,4 @@ $(function() {
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user