Warenwirtschaft: Bestandsrechnung auf der Rohstoff-Detailseite nachvollziehbar

Neuer Klappbereich „Bestandsrechnung nachvollziehen" zeigt alle gebuchten
Eingänge – auch aufgebrauchte mit Rest 0 – mit Produktionsverbrauch,
Ausschuss mit Chargenbezug und dem nach FEFO zugerechneten Ausschuss ohne
Chargenbezug; Summenzeile bis zum Gesamtbestand identisch zur Kopfzahl.
Alt-Ausschuss ohne Chargenbezug wird einzeln mit Grund/Notiz/Bearbeiter
gelistet. Anlass: Live-Fall Sonnenblumenöl (doppelte Korrektur vom 28.07.).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-17 10:34:04 +00:00
parent 79bec0ff0b
commit 761ea35047
7 changed files with 417 additions and 2 deletions

View file

@ -326,6 +326,20 @@ dadurch für die Bedienung ändert:
Rohstoff/Artikel gesperrt. Auch Änderungen an offenen Bestellungen werden
protokolliert (Grund dort optional).
### Neu 17.09.2026 (Rohstoff-Detail: Bestandsrechnung nachvollziehen)
- **Klappbereich „Bestandsrechnung nachvollziehen“** unten auf der Rohstoff-Detailseite:
listet **alle** gebuchten Eingänge – auch die, die in „Verfügbare Chargen“ nicht mehr
erscheinen, weil ihr Rest 0 ist („aufgebraucht“) – mit Eingangsmenge, Produktionsverbrauch,
Ausschuss mit Chargenbezug und dem rechnerisch zugeordneten **Ausschuss ohne Chargenbezug**.
Die Summenzeile (Eingänge − Produktion − Ausschuss = Gesamtbestand) entspricht exakt der
Kopfzahl. Ausschuss-Buchungen ohne Chargenbezug (Altbestand) werden darunter einzeln mit
Datum, Grund, Notiz und Bearbeiter gezeigt.
- **Hintergrund:** Ausschuss ohne Chargenbezug mindert den Gesamtbestand und wird in der
Chargenansicht nach MHD (ältestes zuerst) auf die Eingänge verteilt – dadurch können auch
neuere Eingänge als aufgebraucht erscheinen. Ist so eine Buchung überholt (z. B. eine
doppelte Korrektur), lässt sie sich in der Ausschuss-Liste löschen.
### In Vorbereitung / geplant
- **Versand im System** (DHL-Etiketten per API, AP-32 — bis dahin gilt die

View file

@ -1,6 +1,6 @@
# Testleitfaden Warenwirtschaft — Version 3.2
# Testleitfaden Warenwirtschaft — Version 3.3
> **Stand:** 14.09.2026
> **Stand:** 17.09.2026
> Dieser Leitfaden führt einmal durch die Funktionen der Warenwirtschaft — in der Reihenfolge eines typischen Arbeitsablaufs, sodass jede Stelle direkt im Browser nachgeschaut und getestet werden kann.
> Die fachlichen Erklärungen dazu stehen wie gewohnt in der App unter den **[Hinweisen zur Warenwirtschaft](/admin/inventory/notices)**.
>
@ -436,6 +436,20 @@
---
## 28. Rohstoff-Detail: Bestandsrechnung nachvollziehen (17.09.2026)
**Wo:** **[Rohstoffbestand](/admin/inventory/raw-material-stock)** → Rohstoff öffnen → unten **„Bestandsrechnung nachvollziehen“** (aufklappen).
**Neu:** Alle gebuchten Eingänge – auch aufgebrauchte (Rest 0), die in „Verfügbare Chargen“ nicht mehr stehen – mit Eingang, Produktion, Ausschuss (mit/ohne Chargenbezug) und Rest je Eingang; darunter die Summenzeile bis zum Gesamtbestand und die Liste der Ausschuss-Buchungen ohne Chargenbezug.
**So testen:**
1. Rohstoff mit zwei Eingängen (z. B. 500 g MHD 2027, 1.000 g MHD 2028). Ausschuss **ohne Charge** über 700 g buchen → Chargenansicht zeigt nur noch den zweiten Eingang mit 800 g; im Klappbereich stehen **beide** Eingänge: erster „− 500 g / 0 g, aufgebraucht“, zweiter „− 200 g / 800 g“, Summenzeile „1.500 − 0 − 0 − 700 = 800 g“ = Kopfzahl.
2. Kopfzeile des Klappbereichs zeigt „2 Eingänge, 1 davon aufgebraucht, 1 Ausschuss ohne Chargenbezug“; die Ausschuss-Buchung steht mit Datum, Grund, Notiz und Bearbeiter darunter.
3. Aus dem zweiten Eingang 100 g produzieren → Spalte „Produktion“ − 100 g, Rest 700 g, Summenzeile entsprechend.
4. Eingang in der Tabelle anklicken → öffnet die Detailseite des Wareneingangs.
---
## Hinweis am Rande
Die Listen **Einkauf & Wareneingang**, **Produktionen** und die **Produktbestand-Historie** zeigen aus Performance-Gründen die neuesten 500 Einträge; darüber erscheint ein Link **„Alle anzeigen"**. Offene Bestellungen sind immer vollständig gelistet.

View file

@ -306,5 +306,160 @@
</table>
</div>
</div>
{{-- 17.09.2026: Bestandsrechnung zum Nachvollziehen — alle Eingänge (auch Rest 0), Verbrauch, Ausschuss --}}
@php
$bd = $breakdown;
$unboundCount = $bd['unbound_disposals']->count();
$zeroCount = $bd['entries']->filter(fn ($e) => (float) $e->getAttribute('remaining_quantity') <= 0)->count();
$summaryParts = [trans_choice(':count Eingang|:count Eingänge', $bd['entries']->count(), ['count' => $bd['entries']->count()])];
if ($zeroCount > 0) {
$summaryParts[] = $zeroCount . ' ' . __('davon aufgebraucht');
}
if ($unboundCount > 0) {
$summaryParts[] = $unboundCount . ' ' . __('Ausschuss ohne Chargenbezug');
}
@endphp
<div class="wawi-card">
<a class="wawi-card__header d-flex justify-content-between align-items-center text-reset text-decoration-none collapsed"
data-toggle="collapse" href="#stock-breakdown" role="button" aria-expanded="false" aria-controls="stock-breakdown">
<span>
{{ __('Bestandsrechnung nachvollziehen') }}
<span class="text-muted small font-weight-normal ml-2">
{{ implode(', ', $summaryParts) }}
</span>
</span>
<span class="fas fa-chevron-down small text-muted"></span>
</a>
<div class="collapse" id="stock-breakdown">
<div class="table-responsive">
<table class="table wawi-table mb-0">
<thead>
<tr>
<th>{{ __('Eingang') }}</th>
<th>{{ __('Charge') }}</th>
<th>{{ __('Lagerort') }}</th>
<th>{{ __('MHD') }}</th>
<th class="text-right">{{ __('Eingang') }}</th>
<th class="text-right">{{ __('Produktion') }}</th>
<th class="text-right" title="{{ __('Ausschuss, der beim Erfassen dieser Charge zugeordnet wurde') }}">{{ __('Ausschuss (Charge)') }}</th>
<th class="text-right" title="{{ __('Ausschuss ohne Chargenbezug — wird rechnerisch nach MHD (FEFO) auf die Eingänge verteilt') }}">{{ __('Ausschuss (o. Charge)') }}</th>
<th class="text-right">{{ __('Rest') }}</th>
</tr>
</thead>
<tbody>
@forelse($bd['entries'] as $entry)
@php $isEmpty = (float) $entry->getAttribute('remaining_quantity') <= 0; @endphp
<tr class="{{ $isEmpty ? 'text-muted' : '' }}">
<td>
<a href="{{ route('admin.inventory.stock-entries.show', $entry) }}" class="text-reset">
{{ $entry->received_at?->format('d.m.Y') ?? '—' }}
</a>
<span class="text-muted small">#{{ $entry->id }}</span>
@if($entry->supplier)
<div class="wawi-item-sub">{{ $entry->supplier->name }}</div>
@endif
</td>
<td>{{ $entry->batch_number ?: '—' }}</td>
<td>{{ $entry->location?->name ?? '—' }}</td>
<td>{{ $entry->best_before?->format('d.m.Y') ?? '—' }}</td>
<td class="text-right">{{ formatWawiQuantity($entry->received_quantity) }} g</td>
<td class="text-right">{{ (float) $entry->getAttribute('production_quantity') > 0 ? '− ' . formatWawiQuantity($entry->getAttribute('production_quantity')) . ' g' : '—' }}</td>
<td class="text-right">{{ (float) $entry->getAttribute('bound_disposal_quantity') > 0 ? '− ' . formatWawiQuantity($entry->getAttribute('bound_disposal_quantity')) . ' g' : '—' }}</td>
<td class="text-right">{{ (float) $entry->getAttribute('unbound_disposal_quantity') > 0 ? '− ' . formatWawiQuantity($entry->getAttribute('unbound_disposal_quantity')) . ' g' : '—' }}</td>
<td class="text-right">
<strong>{{ formatWawiQuantity($entry->getAttribute('remaining_quantity')) }} g</strong>
@if($isEmpty)
<span class="badge badge-light border ml-1">{{ __('aufgebraucht') }}</span>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="9"><div class="wawi-empty">{{ __('Noch kein gebuchter Wareneingang.') }}</div></td>
</tr>
@endforelse
</tbody>
<tfoot>
<tr>
<td colspan="4" class="text-right text-muted">{{ __('Summe Wareneingänge') }}</td>
<td class="text-right">{{ formatWawiQuantity($bd['received_total']) }} g</td>
<td colspan="4"></td>
</tr>
<tr>
<td colspan="4" class="text-right text-muted">{{ __('− Produktionsverbrauch') }}</td>
<td class="text-right">− {{ formatWawiQuantity($bd['production_total']) }} g</td>
<td colspan="4" class="text-muted small">
@if($bd['production_other'] > 0)
{{ __('davon :qty g ohne zuordenbaren Eingang (z. B. Eingang nachträglich gelöscht)', ['qty' => formatWawiQuantity($bd['production_other'])]) }}
@endif
</td>
</tr>
<tr>
<td colspan="4" class="text-right text-muted">{{ __('− Ausschuss mit Chargenbezug') }}</td>
<td class="text-right">− {{ formatWawiQuantity($bd['disposed_bound_total'] + $bd['disposed_other']) }} g</td>
<td colspan="4" class="text-muted small">
@if($bd['disposed_other'] > 0)
{{ __('davon :qty g ohne zuordenbaren Eingang (Eingang nachträglich gelöscht)', ['qty' => formatWawiQuantity($bd['disposed_other'])]) }}
@endif
</td>
</tr>
<tr>
<td colspan="4" class="text-right text-muted">{{ __('− Ausschuss ohne Chargenbezug') }}</td>
<td class="text-right">− {{ formatWawiQuantity($bd['disposed_unbound_total']) }} g</td>
<td colspan="4" class="text-muted small">
@if($bd['disposed_unbound_unallocated'] > 0)
{{ __('davon :qty g keinem Eingang zurechenbar (mehr Ausschuss als Eingang am Lagerort)', ['qty' => formatWawiQuantity($bd['disposed_unbound_unallocated'])]) }}
@endif
</td>
</tr>
<tr>
<td colspan="4" class="text-right"><strong>{{ __('= Gesamtbestand') }}</strong></td>
<td class="text-right"><strong>{{ formatWawiQuantity($bd['remaining_total']) }} g</strong></td>
<td colspan="4"></td>
</tr>
</tfoot>
</table>
</div>
@if($bd['unbound_disposals']->isNotEmpty())
<div class="wawi-card__footer" style="display:block;">
<div class="font-weight-bold mb-2">{{ __('Ausschuss ohne Chargenbezug') }}</div>
<p class="text-muted small mb-2">
{{ __('Diese Buchungen wurden ohne Zuordnung zu einem Eingang erfasst (Altbestand vor Einführung der Chargenwahl). Sie mindern den Gesamtbestand und werden in der Chargenansicht rechnerisch nach MHD (ältestes zuerst) von den Eingängen abgezogen — deshalb können auch neuere Eingänge als „aufgebraucht" erscheinen. Ist so eine Buchung überholt (z. B. doppelte Korrektur), lässt sie sich in der Ausschuss-Liste löschen.') }}
</p>
<div class="table-responsive">
<table class="table wawi-table table-sm mb-0">
<thead>
<tr>
<th>{{ __('Datum') }}</th>
<th>{{ __('Lagerort') }}</th>
<th>{{ __('Grund') }}</th>
<th>{{ __('Notiz') }}</th>
<th>{{ __('Erfasst von') }}</th>
<th class="text-right">{{ __('Menge') }}</th>
</tr>
</thead>
<tbody>
@foreach($bd['unbound_disposals'] as $disposal)
<tr>
<td>{{ $disposal->disposed_at?->format('d.m.Y') ?? $disposal->created_at?->format('d.m.Y') }} <span class="text-muted small">#{{ $disposal->id }}</span></td>
<td>{{ $disposal->location?->name ?? '—' }}</td>
<td>{{ $disposal->reason ?: '—' }}</td>
<td>{{ $disposal->note ?: '—' }}</td>
<td>{{ $disposal->user?->name ?? '—' }}</td>
<td class="text-right">− {{ formatWawiQuantity($disposal->quantity) }} g</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@if(Auth::user()->isAdmin())
<a href="{{ route('admin.inventory.stock-disposals.index') }}" class="btn btn-sm btn-outline-secondary mt-2">{{ __('Zur Ausschuss-Liste') }}</a>
@endif
</div>
@endif
</div>
</div>
</div>
@endsection