@php $d = $proposal->algorithm_details ?? []; $t = $d['trace'] ?? []; $hasTrace = ! empty($t); // ── Formatierer (deutsche Schreibweise) ───────────────────────────── $nf0 = fn ($v) => number_format((float) $v, 0, ',', '.'); // Für die Mengenherleitung: eine Nachkommastelle, glatte Werte ohne. Ohne // das las sich „2,4 − 1,0 = 1,4 → 2" auf dem Schirm als „2 − 1 = 1 → 2“. $nf1 = fn ($v) => rtrim(rtrim(number_format((float) $v, 1, ',', '.'), '0'), ','); $nf2 = fn ($v) => number_format((float) $v, 2, ',', '.'); // ── Kernwerte (Trace bevorzugt, sonst aus gespeicherten Feldern) ──── $mu = (float) $proposal->avg_daily_demand; // Ø Bedarf/Tag $sigma = (float) $proposal->demand_std_dev; // Prognosefehler σ $ss = (float) $proposal->safety_stock; // Sicherheitsbestand (effektiv) $rop = (float) $proposal->reorder_point; // Meldebestand s $ip = (float) $proposal->expected_stock; // Bestandsposition IP $oq = (float) $proposal->order_quantity; // Bestellmenge (final) $cur = (float) $proposal->current_stock; // physischer Bestand $leadDays = (int) $proposal->lead_time_days; $sl = (int) $proposal->service_level; $z = (float) ($d['z_factor'] ?? 0); // s = μτ + SS ⇒ μτ ist exakt aus s und SS rekonstruierbar $muTau = (float) ($t['mu_tau'] ?? max(0, $rop - $ss)); $ssStat = array_key_exists('safety_stock_stat', $t) ? (float) $t['safety_stock_stat'] : null; $nTau = (int) ($t['selling_days_tau'] ?? 0); $tauDays = (int) ($t['protection_days'] ?? $leadDays); $orderUpTo= (float) ($t['order_up_to'] ?? ($ip + $oq)); // S (bei fehlendem Trace genähert) $cycle = max(0, $orderUpTo - $rop); $pack = max(1.0, (float) ($t['pack_size'] ?? 1)); $inTransit= (float) ($t['in_transit'] ?? max(0, $ip - $cur)); $reserved = (float) ($t['reserved'] ?? 0); $whAvail = array_key_exists('warehouse_available', $t) ? $t['warehouse_available'] : ($d['warehouse_available'] ?? null); $maxStock = $t['max_stock'] ?? null; $minStock = $t['min_stock'] ?? null; $display = $t['display_stock'] ?? null; $shelf = $t['shelf_capacity'] ?? null; $corrected= (int) ($d['corrected_days'] ?? 0); $targetDays = $t['target_coverage_days'] ?? null; $arrival = $d['arrival_date'] ?? null; $arrivalFmt = $arrival ? \Illuminate\Support\Carbon::parse($arrival)->format('d.m.Y') : null; // ── Mengenherleitung (Rohbedarf → Gebinde → Deckelung) ────────────── // Rohbedarf und gerundete Menge kommen aus der Rechenspur; nur wenn die // fehlt (Vorschläge aus älteren Läufen), werden sie nachgebildet. $roundUpFrom = (float) ($t['pack_round_up_from'] ?? 0); $rawNeed = (float) ($t['raw_need'] ?? max(0, $orderUpTo - $ip)); if (array_key_exists('pack_rounded', $t)) { $packRounded = (float) $t['pack_rounded']; } else { $vielfache = $rawNeed / $pack; $rest = $vielfache - floor($vielfache); $packRounded = ($rest > 1e-9 && $rest < $roundUpFrom ? floor($vielfache) : ceil($vielfache)) * $pack; } $caps = ['Gebinde-Rohbedarf' => $packRounded]; if ($whAvail !== null) $caps['Zentrallager frei'] = max(0, (float) $whAvail); if ($maxStock !== null && $maxStock>0) $caps['Max-Bestand − Position'] = max(0, (float) $maxStock - $ip); if ($shelf !== null) $caps['Regalplatz − Bestand'] = max(0, (float) $shelf - $cur); $binding = array_keys($caps, min($caps))[0] ?? 'Gebinde-Rohbedarf'; $oosDays = $proposal->stockout_date ? (int) today()->diffInDays($proposal->stockout_date) : null; // ── Gauge-Skala ───────────────────────────────────────────────────── $gMax = max($orderUpTo, $ip, $rop, $ss, 1) * 1.08; $GX0 = 40; $GX1 = 660; $gx = fn ($v) => $GX0 + (max(0, min($gMax, (float) $v)) / $gMax) * ($GX1 - $GX0); @endphp
{{-- Header --}}

{{ $proposal->branch->name }}

{{ $proposal->article_name }}

SKU: {{ $proposal->article_sku ?: '–' }} · Berechnet: {{ $proposal->proposal_date->format('d.m.Y') }}

Lieferant: {{ $proposal->supplier_name ?: 'KEINER HINTERLEGT – nicht bestellbar' }} @if($proposal->supplier_id) · {{ ($d['stock_source'] ?? null) === 'central_warehouse' ? 'aus Zentrallager ' . ($d['warehouse_id'] ?? '–') : 'Direktlieferung, keine Bestandsdeckelung' }} @endif

@if($proposal->status === 'submitted') Übermittelt @elseif($proposal->status === 'failed') Fehlgeschlagen @elseif($proposal->status === 'pending') Ausstehend @else Übersprungen @endif

Bestand

{{ $nf0($cur) }}

Bestandslage

{{ $proposal->severityLabel() }}

@if(!is_null($proposal->days_of_stock) && $proposal->days_of_stock > 0)

reicht ~{{ $proposal->days_of_stock }} Tage

@endif

Transfermenge

{{ $nf0($oq) }}

Lieferzeit

{{ $leadDays }} Tage

{{-- Klartext-Fazit --}}

Warum diese Menge?

Die Filiale verkauft im Schnitt {{ $nf2($mu) }} Stück pro Tag. Bis frische Ware @if($arrivalFmt) am {{ $arrivalFmt }} @endif eintrifft (Schutzzeitraum {{ $tauDays }} Tage), werden voraussichtlich {{ $nf1($muTau) }} Stück gebraucht. Damit in dieser Zeit trotz Schwankungen nichts ausgeht, kommt ein Sicherheitsbestand von {{ $nf1($ss) }} Stück dazu – zusammen der Meldebestand {{ $nf1($rop) }}. Aktuell verfügbar sind nur {{ $nf1($ip) }} Stück (Bestand + Zulauf − Reserviert), also unter dem Meldebestand → nachbestellen. Aufgefüllt wird bis zum Zielbestand {{ $nf1($orderUpTo) }}; nach Gebinderundung @if($binding !== 'Gebinde-Rohbedarf') und Begrenzung durch „{{ $binding }}" @endif ergibt das {{ $nf0($oq) }} Stück.

{{-- Zusammensetzung & Entscheidung --}}

So kommt Meldebestand & Ziel zustande

Der Meldebestand s ist der Bedarf bis zur Lieferung plus Sicherheitsbestand. Unterschreitet die Position ihn, wird bis zum Zielbestand S aufgefüllt.

@php $S = max((float) $orderUpTo, 0.0001); $segMu = max(0.0, (float) $muTau); $segSS = max(0.0, (float) $ss); $segCyc = max(0.0, $S - $segMu - $segSS); $PAD_L = 8; $PAD_R = 692; $sx = fn ($v) => $PAD_L + (max(0.0, min($S, (float) $v)) / $S) * ($PAD_R - $PAD_L); $xMu = $sx($segMu); $xS_s = $sx($segMu + $segSS); // Ende SS = Meldebestand s $xEnd = $sx($S); $xIP = $sx($ip); $wMu = $xMu - $PAD_L; $wSS = $xS_s - $xMu; $wCyc = $xEnd - $xS_s; $wGap = $xEnd - $xIP; // auffüllen bis Ziel @endphp {{-- Balken 1: Zusammensetzung des Zielbestands --}}

Zielbestand S

{{ $nf0($S) }} Stk

Zusammensetzung des Zielbestands {{-- Segmente --}} {{-- In-Segment-Werte nur wenn genug Platz --}} @if($wMu > 46){{ $nf0($segMu) }}@endif @if($wSS > 46){{ $nf0($segSS) }}@endif @if($wCyc > 46){{ $nf0($segCyc) }}@endif {{-- Meldebestand-Marke oberhalb (einzige Marke -> keine Kollision) --}} s = {{ $nf0($rop) }} {{-- Legende --}}
Bedarf bis Lieferung (μτ) {{ $nf0($segMu) }} Sicherheitsbestand (SS) {{ $nf0($segSS) }} Zyklusbedarf {{ $nf0($segCyc) }}
{{-- Balken 2: Position vs. Ziel --}}

Verfügbar vs. Ziel

Position {{ $nf0($ip) }} · Ziel {{ $nf0($S) }}

Bestandsposition gegenüber Zielbestand {{-- Track --}} {{-- Vorhanden (Position) --}} {{-- Auffüllen (Position -> Ziel) --}} @if($wGap > 60)auffüllen {{ $nf0($rawNeed) }}@endif {{-- Position-Label OBERHALB --}} Position {{ $nf0($ip) }} {{-- Meldebestand-Marke UNTERHALB (andere Ebene -> keine Kollision mit Position) --}} Meldebestand s = {{ $nf0($rop) }}
@if($rawNeed > 0) Position {{ $nf0($ip) }} liegt unter dem Meldebestand {{ $nf0($rop) }} → auffüllen bis Ziel {{ $nf0($S) }}. Rechnerisch fehlen {{ $nf0($rawNeed) }} Stk, nach Gebinde/Deckelung ergibt das {{ $nf0($oq) }} Stk. @else Position {{ $nf0($ip) }} hat den Zielbestand {{ $nf0($S) }} bereits erreicht – rechnerisch besteht kein Auffüllbedarf. @endif
{{-- Schritt-für-Schritt-Herleitung --}} @php $steps = []; $steps[] = [ 'title' => 'Tagesbedarf bestimmen', 'plain' => 'Durchschnittlicher Tagesverkauf – geglättet (Trend berücksichtigt) und um Tage ohne Bestand bereinigt, damit „ausverkauft" nicht als „keine Nachfrage" zählt.', 'formula' => 'μ/Tag = geglätteter Ø-Verkauf (Holt-Verfahren)', 'calc' => 'μ/Tag = ' . $nf2($mu) . ' Stk' . ($corrected ? ' · aus ' . $corrected . ' bereinigten Tagen' : '') . ' · Streuung σ = ' . $nf2($sigma), ]; $steps[] = [ 'title' => 'Schutzzeitraum bis zur Lieferung', 'plain' => 'So lange muss der aktuelle Bestand reichen: von heute bis die nächste Lieferung eintrifft.', 'formula' => 'τ = heute bis Warenankunft', 'calc' => 'τ = ' . $tauDays . ' Kalendertage' . ($nTau ? ' (' . $nTau . ' Verkaufstage)' : '') . ($arrivalFmt ? ' · Ankunft ' . $arrivalFmt : ''), ]; $steps[] = [ 'title' => 'Bedarf im Schutzzeitraum', 'plain' => 'Wie viel wird bis zur Lieferung voraussichtlich verkauft (kalendergewichtet nach Wochentag/Aktion)?', 'formula' => 'μτ = Σ Tagesprognosen ≈ μ/Tag × Verkaufstage', 'calc' => ($nTau ? 'μτ ≈ ' . $nf2($mu) . ' × ' . $nTau . ' = ' . $nf1($mu * $nTau) . ' → ' : '') . 'μτ = ' . $nf1($muTau) . ' Stk', ]; $ssFormulaCalc = $ssStat !== null && $nTau ? 'SS = ' . $nf2($z) . ' × √(' . $nTau . ' × ' . $nf2($sigma) . '²) = ' . $nf1($ssStat) . ' → effektiv (inkl. Display/Min/Schwund) = ' . $nf1($ss) : 'SS = ' . $nf1($ss) . ' Stk'; $steps[] = [ 'title' => 'Sicherheitsbestand', 'plain' => 'Puffer gegen Nachfrageschwankungen. Je höher der gewünschte Servicegrad (' . $sl . ' %), desto größer der Sicherheitsfaktor z' . ($z ? ' = ' . $nf2($z) : '') . '. Untergrenzen: Display-/Mindestbestand plus erwarteter Schwund.', 'formula' => 'SS = z × √(Verkaufstage × σ²), danach max(·, Display, Min) + Schwund', 'calc' => $ssFormulaCalc, ]; $steps[] = [ 'title' => 'Meldebestand s', 'plain' => 'Ab diesem Bestand muss nachbestellt werden.', 'formula' => 's = μτ + SS', 'calc' => 's = ' . $nf1($muTau) . ' + ' . $nf1($ss) . ' = ' . $nf1($rop) . ' Stk', ]; $steps[] = [ 'title' => 'Zielbestand S', 'plain' => 'Bis hierhin wird aufgefüllt – Meldebestand plus Bedarf für den anschließenden Nachbestellzyklus' . ($targetDays ? ' (' . (int) $targetDays . ' Tage Zielreichweite)' : '') . '.', 'formula' => 'S = s + Zyklusbedarf', 'calc' => 'S = ' . $nf1($rop) . ' + ' . $nf1($cycle) . ' = ' . $nf1($orderUpTo) . ' Stk', ]; $steps[] = [ 'title' => 'Auslösung prüfen', 'plain' => 'Bestandsposition = physischer Bestand plus bereits bestellte Ware (Zulauf) minus reservierte Ware.', 'formula' => 'Position = Bestand + Zulauf − Reserviert → bestellen, wenn Position ≤ s', 'calc' => 'Position = ' . $nf1($cur) . ' + ' . $nf1($inTransit) . ' − ' . $nf1($reserved) . ' = ' . $nf1($ip) . ' ≤ s ' . $nf1($rop) . ' → ja', ]; $steps[] = [ 'title' => 'Bestellmenge', 'plain' => $roundUpFrom > 0 ? 'Von der Position bis zum Ziel auffüllen und auf volle Gebinde runden – aufgerundet wird erst ab ' . $nf1($roundUpFrom * 100) . ' % eines Gebindes, darunter abgerundet. Anschließend auf das begrenzen, ' . 'was Zentrallager, Maximalbestand und Regalplatz zulassen.' : 'Von der Position bis zum Ziel auffüllen, auf volle Gebinde aufrunden und auf das begrenzen, was Zentrallager, Maximalbestand und Regalplatz zulassen.', 'formula' => 'Menge = min( runden(S − Position; Gebinde), ZL-frei, Max−Position, Regal−Bestand )', 'calc' => 'S − Position = ' . $nf1($orderUpTo) . ' − ' . $nf1($ip) . ' = ' . $nf1($rawNeed) . ' → Gebinde(' . $nf1($pack) . '): ' . $nf1($packRounded) . ' → Bestellmenge = ' . $nf0($oq) . ' Stk', ]; @endphp

Wie die Menge zustande kommt

    @foreach($steps as $i => $s)
  1. {{ $i + 1 }}

    {{ $s['title'] }}

    {{ $s['plain'] }}

    {{ $s['formula'] }}

    {{ $s['calc'] }}

  2. @endforeach
Ergebnis Bestellmenge = {{ $nf0($oq) }} Stk
@unless($hasTrace)

Hinweis: Dieser Vorschlag stammt aus einer früheren Version – einige Zwischenwerte sind genähert. Für die vollständige Rechenspur den Nachschub neu berechnen.

@endunless
{{-- Deckelungen (nur mit Trace sinnvoll) --}} @if($hasTrace && count($caps) > 1)

Begrenzungen

Die kleinste Grenze bestimmt die Bestellmenge. Bindend war: {{ $binding }}.

@foreach($caps as $label => $capVal)
{{ $label }} {{ $nf0($capVal) }} Stk
@endforeach
@endif {{-- Projizierte Reichweite (bestehende Kurve) --}}

Projizierte Reichweite

Bestandsabbau bei Ø {{ $nf2($mu) }} Stk/Tag. @if($oosDays !== null) Voraussichtlicher Ausverkauf in {{ $oosDays }} Tagen ({{ $proposal->stockout_date->format('d.m.Y') }}). @endif

Bestandsabbau Meldebestand Sicherheitsbestand
{{-- Rohwerte (einklappbar) --}} @if(!empty($d))
Rohwerte (Algorithmus)
@foreach($d as $key => $val) @continue($key === 'trace')
{{ $key }}
{{ is_scalar($val) ? $val : json_encode($val) }}
@endforeach
@endif {{-- Aktionen --}}
Zurück @if($proposal->status === 'pending')
@csrf @method('PATCH')
@endif