Warenwirtschaft: AP-09 bis AP-13 (Produktbestand, Set-Produkte, Ausschuss, Konzepte)
- AP-09 Produktbestand inkl. Bewegungshistorie (product_stock_movements, ProductStockService) - AP-10 Rohstoffbestand-Ansicht je Lager (RawMaterialStockController) - AP-11 Bestandsschwellen / Out-of-Stock-Handling fuer Produkte und Shop - AP-12 Ausgang/Ausschuss (stock_disposals, StockDisposalController, InventoryService) - Set-Produkte (product_set_items) inkl. Aufloesung - Produktentwicklung & Hinweise-Verwaltung (Notices) - AP-13 Entwicklungskonzept Shop-Bestandsabzug im Plan dokumentiert - Feature-Tests fuer neue Module + aktualisierter Entwicklungsplan Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
78679e0c55
commit
3ee2d756e9
63 changed files with 5968 additions and 901 deletions
194
resources/views/admin/inventory/stock-disposals/create.blade.php
Normal file
194
resources/views/admin/inventory/stock-disposals/create.blade.php
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
<h4 class="font-weight-bold py-2 mb-2">{{ __('Ausgang / Ausschuss erfassen') }}</h4>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small">{{ __('Reduziert den Bestand des gewählten Rohstoffs bzw. Verpackungsartikels. Der Grund ist Pflicht und erscheint in der Ausgangsliste.') }}</p>
|
||||
<form method="post" action="{{ route('admin.inventory.stock-disposals.store') }}">
|
||||
@csrf
|
||||
|
||||
<div class="form-group">
|
||||
<label for="disposal_type">{{ __('Art') }} <span class="text-danger">*</span></label>
|
||||
<select name="disposal_type" id="disposal_type" class="form-control" required>
|
||||
<option value="ingredient" @selected(old('disposal_type', $prefill['disposal_type']) === 'ingredient')>{{ __('Rohstoff') }}</option>
|
||||
<option value="packaging" @selected(old('disposal_type', $prefill['disposal_type']) === 'packaging')>{{ __('Verpackung') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="disposal-ingredient-block" class="form-group" style="display:none;">
|
||||
<label for="ingredient_id">{{ __('Rohstoff') }} <span class="text-danger">*</span></label>
|
||||
<div class="light-style">
|
||||
<select name="ingredient_id" id="ingredient_id" class="w-100"
|
||||
data-search-url="{{ route('admin.inventory.api.ingredients.search') }}"
|
||||
data-charges-url="{{ route('admin.inventory.api.disposals.ingredient-charges', ['ingredient' => '__ID__']) }}">
|
||||
@if ($prefill['ingredient_id'])
|
||||
<option value="{{ $prefill['ingredient_id'] }}" selected>{{ $prefill['ingredient_label'] }}</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="disposal-charge-block" class="form-group" style="display:none;">
|
||||
<label for="stock_entry_id">{{ __('Charge') }} <span class="text-muted small">({{ __('optional') }})</span></label>
|
||||
<select name="stock_entry_id" id="stock_entry_id" class="form-control">
|
||||
<option value="">{{ __('— keine bestimmte Charge —') }}</option>
|
||||
</select>
|
||||
<small class="text-muted">{{ __('Bei Auswahl wird der Lagerort automatisch gesetzt.') }}</small>
|
||||
</div>
|
||||
|
||||
<div id="disposal-packaging-block" class="form-group" style="display:none;">
|
||||
<label for="packaging_item_id">{{ __('Verpackungsartikel') }} <span class="text-danger">*</span></label>
|
||||
<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') }}">
|
||||
@if (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="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') === (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="quantity">{{ __('Menge') }} <span class="text-danger">*</span></label>
|
||||
<input type="text" name="quantity" id="quantity" autocomplete="off"
|
||||
class="form-control @error('quantity') is-invalid @enderror" value="{{ old('quantity') }}" required>
|
||||
<small class="text-muted" id="quantity-hint">{{ __('Bei Rohstoff in Gramm, bei Verpackung in Stück.') }}</small>
|
||||
@error('quantity')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="reason">{{ __('Grund') }} <span class="text-danger">*</span></label>
|
||||
<select name="reason" id="reason" class="form-control @error('reason') is-invalid @enderror" required>
|
||||
@foreach ($reasons as $reason)
|
||||
<option value="{{ $reason }}" @selected(old('reason') === $reason)>{{ $reason }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('reason')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="note">{{ __('Hinweis') }}</label>
|
||||
<input type="text" name="note" id="note" maxlength="255" autocomplete="off"
|
||||
class="form-control @error('note') is-invalid @enderror" value="{{ old('note') }}">
|
||||
@error('note')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="disposed_at">{{ __('Datum') }} <span class="text-danger">*</span></label>
|
||||
<input type="text" name="disposed_at" id="disposed_at" autocomplete="off"
|
||||
class="form-control datepicker-base @error('disposed_at') is-invalid @enderror"
|
||||
value="{{ old('disposed_at', now()->format('d.m.Y')) }}" required>
|
||||
@error('disposed_at')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">{{ __('Buchen') }}</button>
|
||||
<a href="{{ route('admin.inventory.stock-disposals.index') }}" class="btn btn-outline-secondary">{{ __('Zurück') }}</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
(function ($) {
|
||||
function toggleBlocks() {
|
||||
var isIng = $('#disposal_type').val() === 'ingredient';
|
||||
$('#disposal-ingredient-block').toggle(isIng);
|
||||
$('#disposal-charge-block').toggle(isIng);
|
||||
$('#disposal-packaging-block').toggle(!isIng);
|
||||
$('#quantity-hint').text(isIng
|
||||
? '{{ __('Menge in Gramm.') }}'
|
||||
: '{{ __('Menge in Stück.') }}');
|
||||
}
|
||||
|
||||
function initSearchSelect2(id, placeholder) {
|
||||
var $el = $('#' + id);
|
||||
if ($el.data('select2')) {
|
||||
$el.select2('destroy');
|
||||
}
|
||||
$el.select2({
|
||||
theme: 'default',
|
||||
width: '100%',
|
||||
placeholder: placeholder,
|
||||
allowClear: true,
|
||||
ajax: {
|
||||
url: $el.data('search-url'),
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
data: function (params) { return {q: params.term || ''}; },
|
||||
processResults: function (data) { return {results: data.results || []}; },
|
||||
cache: true
|
||||
},
|
||||
minimumInputLength: 1
|
||||
});
|
||||
}
|
||||
|
||||
function loadCharges(ingredientId) {
|
||||
var $charge = $('#stock_entry_id');
|
||||
$charge.empty().append($('<option>').val('').text('{{ __('— keine bestimmte Charge —') }}'));
|
||||
if (!ingredientId) {
|
||||
return;
|
||||
}
|
||||
var url = $('#ingredient_id').data('charges-url').replace('__ID__', ingredientId);
|
||||
$.getJSON(url, function (data) {
|
||||
(data.charges || []).forEach(function (c) {
|
||||
$charge.append($('<option>').val(c.id).text(c.text).attr('data-location', c.location_id));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
toggleBlocks();
|
||||
initSearchSelect2('ingredient_id', '{{ __('Rohstoff suchen…') }}');
|
||||
initSearchSelect2('packaging_item_id', '{{ __('Verpackungsartikel suchen…') }}');
|
||||
|
||||
$('#disposal_type').on('change', toggleBlocks);
|
||||
|
||||
$('#ingredient_id').on('change', function () {
|
||||
loadCharges($(this).val());
|
||||
});
|
||||
|
||||
$('#stock_entry_id').on('change', function () {
|
||||
var loc = $(this).find('option:selected').data('location');
|
||||
if (loc) {
|
||||
$('#location_id').val(String(loc));
|
||||
}
|
||||
});
|
||||
|
||||
@if ($prefill['ingredient_id'])
|
||||
loadCharges('{{ $prefill['ingredient_id'] }}');
|
||||
@endif
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
@extends('layouts.layout-2')
|
||||
|
||||
@section('content')
|
||||
@include('admin.inventory.partials.table-actions-style')
|
||||
<div class="card">
|
||||
<div class="card-header d-flex flex-wrap justify-content-between align-items-center">
|
||||
<h6 class="mb-0">{{ __('Ausgang / Ausschuss') }}</h6>
|
||||
<div>
|
||||
<form method="get" class="d-inline-block mr-2">
|
||||
<select name="type" class="form-control form-control-sm d-inline-block" style="width:auto"
|
||||
onchange="this.form.submit()">
|
||||
<option value="">{{ __('Alle Arten') }}</option>
|
||||
<option value="ingredient" @selected($typeFilter === 'ingredient')>{{ __('Rohstoff') }}</option>
|
||||
<option value="packaging" @selected($typeFilter === 'packaging')>{{ __('Verpackung') }}</option>
|
||||
</select>
|
||||
</form>
|
||||
@if (Auth::user()->isAdmin())
|
||||
<a href="{{ route('admin.inventory.stock-disposals.create') }}" class="btn btn-sm btn-primary">
|
||||
{{ __('Ausschuss erfassen') }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-datatable table-responsive">
|
||||
<table class="table table-striped wawi-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __('Datum') }}</th>
|
||||
<th>{{ __('Art') }}</th>
|
||||
<th>{{ __('Artikel') }}</th>
|
||||
<th>{{ __('Charge') }}</th>
|
||||
<th>{{ __('Lagerort') }}</th>
|
||||
<th class="text-right">{{ __('Menge') }}</th>
|
||||
<th>{{ __('Grund') }}</th>
|
||||
<th>{{ __('Hinweis') }}</th>
|
||||
<th>{{ __('Mitarbeiter') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($values as $disposal)
|
||||
<tr>
|
||||
<td>{{ $disposal->disposed_at?->format('d.m.Y') }}</td>
|
||||
<td>
|
||||
@if ($disposal->isIngredient())
|
||||
<span class="badge badge-info">{{ __('Rohstoff') }}</span>
|
||||
@else
|
||||
<span class="badge badge-secondary">{{ __('Verpackung') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ $disposal->articleName() }}</td>
|
||||
<td>
|
||||
@if ($disposal->stockEntry)
|
||||
{{ $disposal->stockEntry->batch_number ?: '#'.$disposal->stockEntry->id }}
|
||||
@else
|
||||
<span class="text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ $disposal->location?->name ?? '—' }}</td>
|
||||
<td class="text-right text-danger font-weight-bold">
|
||||
−{{ \App\Services\Util::formatNumber($disposal->quantity, $disposal->unit === 'piece' ? 0 : 2) }}
|
||||
{{ $disposal->unit === 'piece' ? __('Stück') : 'g' }}
|
||||
</td>
|
||||
<td>{{ $disposal->reason }}</td>
|
||||
<td class="text-muted">{{ $disposal->note }}</td>
|
||||
<td>{{ $disposal->user?->getFullName(false) ?: ($disposal->user?->email ?? '—') }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted py-4">{{ __('Noch keine Ausgänge erfasst.') }}</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
Loading…
Add table
Add a link
Reference in a new issue