gruene-seele/resources/views/admin/inventory/stock-entries/_form.blade.php
Kevin Adametz 736cb087ae Warenwirtschaft: Einkäufe auch nach Buchung korrigierbar, mit Änderungsprotokoll
Bisher ließen sich nur offene Bestellungen bearbeiten; ein Tippfehler in
einem gebuchten Wareneingang war nicht korrigierbar (Anforderung 14.09.2026).

- Bearbeiten/Korrigieren für gebuchte Eingänge (Admin): Bestell- und
  Eingangsdaten (Menge, Datum, Charge, MHD, Qualität, Preise, Lieferant,
  Lagerort). Menge wirkt sofort auf den Bestand; ml-Eingänge werden in ml
  korrigiert und über die gespeicherte Dichte in Gramm gebucht.
- Schutz: Menge nicht unter den bereits verbrauchten Anteil (Rohstoff je
  Eingang, Verpackung je Artikel); bei Verbrauch sind Art und Rohstoff/
  Artikel gesperrt (Formular + Server).
- Änderungsprotokoll (stock_entry_changes): wer/wann/Grund + geänderte
  Felder alt → neu; Anzeige auf der Detailseite. Grund bei gebuchten
  Eingängen Pflicht, bei offenen optional. Speichern ohne Änderung
  erzeugt keine Zeile.
- Tests (6 neue), In-App-Doku (Hinweise + Leitfaden 3.2) nachgezogen.

Migration: 2026_09_14_110000_create_stock_entry_changes_table.
Suite: 396 grün.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 14:51:27 +00:00

294 lines
15 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@php
/** @var \App\Models\StockEntry $model */
$isEdit = $model->exists;
// Anforderung 14.09.2026: gebuchte Eingänge korrigierbar. Wurde daraus schon
// verbraucht, sind Art und Rohstoff/Artikel gesperrt (Bestand bliebe sonst inkonsistent).
$isReceivedEdit = $isEdit && $model->isReceived();
$correctionBlockers = $correctionBlockers ?? [];
$lockItem = $isReceivedEdit && $correctionBlockers !== [];
@endphp
@if($isReceivedEdit)
<div class="alert alert-warning py-2 px-3">
<strong>{{ __('Korrektur eines gebuchten Eingangs.') }}</strong>
{{ __('Änderungen wirken sofort auf den Bestand und werden mit Grund im Änderungsprotokoll festgehalten.') }}
@if($lockItem)
<br>{{ __('Aus diesem Eingang wurde bereits verbraucht (:blockers) — Art und Rohstoff/Artikel sind deshalb gesperrt.', ['blockers' => implode(', ', $correctionBlockers)]) }}
@endif
</div>
@endif
<div class="form-group">
<label for="entry_type">{{ __('Art') }} <span class="text-danger">*</span></label>
@if($lockItem)
<input type="hidden" name="entry_type" value="{{ $model->entry_type }}">
@endif
<select name="entry_type" id="entry_type" class="form-control @error('entry_type') is-invalid @enderror" required @disabled($lockItem)>
@foreach ($entryTypeLabels as $value => $label)
<option value="{{ $value }}" @selected(old('entry_type', $model->entry_type ?? 'ingredient') === $value)>{{ $label }}</option>
@endforeach
</select>
@error('entry_type')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div id="stock-entry-ingredient-block" class="form-group" style="display:none;">
<label for="ingredient_id">{{ __('Inhaltsstoff') }} <span class="text-danger">*</span></label>
@if($lockItem && $model->ingredient_id)
<input type="hidden" name="ingredient_id" value="{{ $model->ingredient_id }}">
@endif
<div class="light-style">
<select name="ingredient_id" id="ingredient_id" class="w-100"
data-search-url="{{ route('admin.inventory.api.ingredients.search') }}" @disabled($lockItem)>
@if ($model->ingredient_id && $model->ingredient)
{{-- N-06: fehlt die Stammdaten-Dichte inzwischen, gilt der Dichte-Snapshot der Bestellung --}}
<option value="{{ $model->ingredient_id }}" data-density="{{ $model->ingredient->density ?? $model->density_used }}" selected>{{ $model->ingredient->name }}@if ($model->ingredient->inci)
({{ $model->ingredient->inci }})
@endif
</option>
@elseif(old('ingredient_id'))
<option value="{{ old('ingredient_id') }}" selected>{{ old('ingredient_id') }}</option>
@endif
</select>
</div>
@error('ingredient_id')
<div class="text-danger small">{{ $message }}</div>
@enderror
</div>
<div id="stock-entry-quality-block" class="form-group" style="display:none;">
<label for="quality_id">{{ __('Rohstoffqualität') }} <span class="text-danger">*</span></label>
<select name="quality_id" id="quality_id" class="form-control @error('quality_id') is-invalid @enderror">
<option value="">{{ __('Bitte wählen') }}</option>
@foreach ($materialQualities as $mq)
<option value="{{ $mq->id }}" @selected((string) old('quality_id', $model->quality_id) === (string) $mq->id)>{{ $mq->name }}</option>
@endforeach
</select>
@error('quality_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div id="stock-entry-packaging-block" class="form-group" style="display:none;">
<label for="packaging_item_id">{{ __('Verpackungsartikel') }} <span class="text-danger">*</span></label>
@if($lockItem && $model->packaging_item_id)
<input type="hidden" name="packaging_item_id" value="{{ $model->packaging_item_id }}">
@endif
<div class="light-style">
<select name="packaging_item_id" id="packaging_item_id" class="w-100"
data-search-url="{{ route('admin.inventory.api.packaging-items.search') }}" @disabled($lockItem)>
@if ($model->packaging_item_id && $model->packagingItem)
<option value="{{ $model->packaging_item_id }}" selected>{{ $model->packagingItem->name }}</option>
@elseif(old('packaging_item_id'))
<option value="{{ old('packaging_item_id') }}" selected>{{ old('packaging_item_id') }}</option>
@endif
</select>
</div>
@error('packaging_item_id')
<div class="text-danger small">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="supplier_id">{{ __('Lieferant') }} <span class="text-danger">*</span></label>
<select name="supplier_id" id="supplier_id" class="form-control @error('supplier_id') is-invalid @enderror"
required>
<option value="">{{ __('Bitte wählen') }}</option>
@foreach ($suppliers as $sup)
<option value="{{ $sup->id }}" @selected((string) old('supplier_id', $model->supplier_id) === (string) $sup->id)>{{ $sup->name }}</option>
@endforeach
</select>
@error('supplier_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="location_id">{{ __('Lagerort') }} <span class="text-danger">*</span></label>
<select name="location_id" id="location_id" class="form-control @error('location_id') is-invalid @enderror"
required>
<option value="">{{ __('Bitte wählen') }}</option>
@foreach ($locations as $loc)
<option value="{{ $loc->id }}" @selected((string) old('location_id', $model->location_id) === (string) $loc->id)>{{ $loc->name }}</option>
@endforeach
</select>
@error('location_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="ordered_at">{{ __('Bestelldatum') }} <span class="text-danger">*</span></label>
<input type="text" name="ordered_at" id="ordered_at" autocomplete="off"
class="form-control datepicker-base @error('ordered_at') is-invalid @enderror"
value="{{ old('ordered_at', $model->ordered_at ? $model->ordered_at->format('d.m.Y') : '') }}" required>
@error('ordered_at')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
@php
// AP-42: in ml erfasste Rohstoffe im Formular auch in ml anzeigen (Rückrechnung
// über die gespeicherte Dichte); Bestand bleibt intern in Gramm.
$orderedIsMl = $model->purchase_unit === 'ml' && $model->density_used;
$orderedDisplay = $model->ordered_quantity !== null
? formatWawiQuantity($orderedIsMl ? $model->ordered_quantity / $model->density_used : $model->ordered_quantity)
: '';
@endphp
@php
// Einheit steht als eigenes Pflichtfeld VOR der Menge (nicht mehr als Anhang
// rechts am Feld — auf breiten Monitoren lag es außer Sicht). Neu: bewusst
// wählen („Bitte wählen"); beim Bearbeiten ist die gespeicherte Einheit vorbelegt.
$orderedUnitValue = old('ordered_unit', $isEdit ? ($orderedIsMl ? 'ml' : 'gram') : '');
@endphp
<div class="form-group" id="ordered-unit-wrap" style="display:none;">
<label for="ordered_unit">{{ __('Einheit der bestellten Menge') }} <span class="text-danger">*</span></label>
<select name="ordered_unit" id="ordered_unit" class="form-control @error('ordered_unit') is-invalid @enderror">
<option value="" @selected($orderedUnitValue === '')>{{ __('Bitte wählen') }}</option>
<option value="gram" @selected($orderedUnitValue === 'gram')>{{ __('Gramm (g)') }}</option>
<option value="ml" @selected($orderedUnitValue === 'ml')>{{ __('Milliliter (ml) – wird über die Dichte in Gramm umgerechnet') }}</option>
</select>
<small class="text-muted d-block" id="ordered-unit-hint"></small>
@error('ordered_unit')
<div class="invalid-feedback d-block">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="ordered_quantity">{{ __('Bestellte Menge') }} <span class="text-danger">*</span></label>
<input type="text" name="ordered_quantity" id="ordered_quantity"
class="form-control @error('ordered_quantity') is-invalid @enderror"
value="{{ old('ordered_quantity', $orderedDisplay) }}"
required>
<small class="text-muted d-block" id="ordered-quantity-hint">{{ __('Bei Rohstoff in Gramm, bei Verpackung in Stück.') }}</small>
<small class="d-block font-weight-bold text-body mt-1" id="ordered-quantity-preview" style="display:none;"></small>
@error('ordered_quantity')
<div class="invalid-feedback d-block">{{ $message }}</div>
@enderror
</div>
<div id="price-per-kg-block" style="display:none;">
<div class="form-group">
<label for="tax_rate_id">{{ __('Umsatzsteuer') }}</label>
<select name="tax_rate_id" id="tax_rate_id" class="form-control @error('tax_rate_id') is-invalid @enderror">
<option value="" data-percent="0">{{ __('— keine Angabe —') }}</option>
@foreach ($taxRates as $taxRate)
<option value="{{ $taxRate->id }}" data-percent="{{ $taxRate->percent }}"
@selected((string) old('tax_rate_id', $model->tax_rate_id) === (string) $taxRate->id)>
{{ $taxRate->name }} ({{ \App\Services\Util::formatNumber($taxRate->percent) }} %)
</option>
@endforeach
</select>
<small class="text-muted">{{ __('Steuersatz zur Umrechnung zwischen Netto und Brutto.') }}</small>
@error('tax_rate_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="price_per_kg">{{ __('Netto-Preis pro kg') }} <span class="text-danger">*</span></label>
<div class="input-group">
<input type="text" name="price_per_kg" id="price_per_kg"
class="form-control @error('price_per_kg') is-invalid @enderror"
value="{{ old('price_per_kg', $model->price_per_kg !== null ? \App\Services\Util::formatNumber($model->price_per_kg) : '') }}">
<div class="input-group-append"><span class="input-group-text">€</span></div>
@error('price_per_kg')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="form-group col-md-6">
<label for="price_per_kg_gross">{{ __('Brutto-Preis pro kg') }}</label>
<div class="input-group">
<input type="text" name="price_per_kg_gross" id="price_per_kg_gross"
class="form-control @error('price_per_kg_gross') is-invalid @enderror"
value="{{ old('price_per_kg_gross', $model->price_per_kg_gross !== null ? \App\Services\Util::formatNumber($model->price_per_kg_gross) : '') }}">
<div class="input-group-append"><span class="input-group-text">€</span></div>
@error('price_per_kg_gross')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
</div>
<small class="text-muted d-block mb-2">{{ __('Netto oder Brutto genügt – der jeweils andere Wert wird automatisch berechnet.') }}</small>
</div>
<div id="price-total-block" class="form-group" style="display:none;">
<label for="price_total">{{ __('Gesamtpreis netto') }} <span class="text-danger">*</span></label>
<input type="text" name="price_total" id="price_total"
class="form-control @error('price_total') is-invalid @enderror"
value="{{ old('price_total', $model->price_total !== null ? \App\Services\Util::formatNumber($model->price_total) : '') }}">
@error('price_total')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
@if($isReceivedEdit)
@php
$receivedDisplay = $model->received_quantity !== null
? formatWawiQuantity($orderedIsMl ? $model->received_quantity / $model->density_used : $model->received_quantity)
: '';
@endphp
<hr>
<h6 class="font-weight-bold">{{ __('Wareneingang (gebucht)') }}</h6>
<div class="form-group">
<label for="received_at">{{ __('Eingangsdatum') }} <span class="text-danger">*</span></label>
<input type="text" name="received_at" id="received_at" autocomplete="off" required
class="form-control datepicker-base @error('received_at') is-invalid @enderror"
value="{{ old('received_at', $model->received_at?->format('d.m.Y')) }}">
@error('received_at')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="received_quantity">{{ __('Eingegangene Menge') }} <span class="text-danger">*</span></label>
<input type="text" name="received_quantity" id="received_quantity" required
class="form-control @error('received_quantity') is-invalid @enderror"
value="{{ old('received_quantity', $receivedDisplay) }}">
<small class="text-muted d-block" id="received-quantity-hint">{{ $model->unit === 'piece' ? __('Angabe in Stück.') : __('Angabe in der oben gewählten Einheit.') }}</small>
<small class="d-block font-weight-bold text-body mt-1" id="received-quantity-preview" style="display:none;"></small>
@error('received_quantity')
<div class="invalid-feedback d-block">{{ $message }}</div>
@enderror
</div>
<div id="received-ingredient-block" style="display:none;">
<div class="form-group">
<label for="batch_number">{{ __('Chargennummer') }} <span class="text-danger">*</span></label>
<input type="text" name="batch_number" id="batch_number" maxlength="100"
class="form-control @error('batch_number') is-invalid @enderror"
value="{{ old('batch_number', $model->batch_number) }}">
@error('batch_number')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="best_before">{{ __('Mindesthaltbarkeit') }} <span class="text-danger">*</span></label>
<input type="text" name="best_before" id="best_before" autocomplete="off"
class="form-control datepicker-base @error('best_before') is-invalid @enderror"
value="{{ old('best_before', $model->best_before?->format('d.m.Y')) }}">
@error('best_before')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
@endif
@if($isEdit)
<hr>
<div class="form-group">
<label for="change_reason">{{ __('Grund der Änderung') }} @if($isReceivedEdit)<span class="text-danger">*</span>@endif</label>
<input type="text" name="change_reason" id="change_reason" maxlength="500" @required($isReceivedEdit)
class="form-control @error('change_reason') is-invalid @enderror"
value="{{ old('change_reason') }}" placeholder="{{ __('z. B. Tippfehler bei der Menge, Lieferschein nachgetragen') }}">
<small class="text-muted d-block">{{ __('Wird mit Datum, Bearbeiter und den geänderten Werten im Änderungsprotokoll des Eintrags gespeichert.') }}</small>
@error('change_reason')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
@endif