Warenwirtschaft: AP-48 — Produktbestand-Buchungsgründe pflegbar, getrennt Ein/Aus
Kundenentscheid 26.08. (D-5): Eingang und Ausgang haben eigene, pflegbare Gründe-Listen statt einer festen Liste im Controller. - Neues Modell/Tabelle stock_movement_reasons (label, direction in/out, pos, active, SoftDeletes) inkl. Seed der bisherigen Gründe sinnvoll auf Ein-/Ausgang aufgeteilt. - CRUD wie DisposalReason (SettingCrudController + FormRequest + Formular mit Richtungs-Auswahl); zwei Listen (Eingangs-/Ausgangs-Gründe) auf der Seite Einstellungen → Allgemeines, „Neu anlegen" mit vorbelegter Richtung. - Produktbestand-Maske: das Grund-Dropdown wird jetzt je nach Eingang/Ausgang per JS aus den passenden, gepflegten Gründen befüllt (manualReasons entfernt). - DeletionGuard: StockMovementReason immer löschbar. Migration create_stock_movement_reasons auf Dev-/Live-DB. 4 Tests, Suite 326 grün. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e08c8dec40
commit
190713c21b
12 changed files with 381 additions and 22 deletions
|
|
@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin\Inventory;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\DeliveryTime;
|
use App\Models\DeliveryTime;
|
||||||
use App\Models\DisposalReason;
|
use App\Models\DisposalReason;
|
||||||
|
use App\Models\StockMovementReason;
|
||||||
use App\Models\StorageRoom;
|
use App\Models\StorageRoom;
|
||||||
use App\Models\StorageShelf;
|
use App\Models\StorageShelf;
|
||||||
use App\Models\StorageSlot;
|
use App\Models\StorageSlot;
|
||||||
|
|
@ -19,6 +20,9 @@ class GeneralSettingController extends Controller
|
||||||
'taxRates' => TaxRate::query()->orderBy('pos')->orderBy('percent')->get(),
|
'taxRates' => TaxRate::query()->orderBy('pos')->orderBy('percent')->get(),
|
||||||
'deliveryTimes' => DeliveryTime::query()->orderBy('pos')->orderBy('label')->get(),
|
'deliveryTimes' => DeliveryTime::query()->orderBy('pos')->orderBy('label')->get(),
|
||||||
'disposalReasons' => DisposalReason::query()->orderBy('pos')->orderBy('label')->get(),
|
'disposalReasons' => DisposalReason::query()->orderBy('pos')->orderBy('label')->get(),
|
||||||
|
// AP-48: Buchungsgründe Produktbestand, getrennt nach Eingang/Ausgang.
|
||||||
|
'stockReasonsIn' => StockMovementReason::query()->where('direction', 'in')->orderBy('pos')->orderBy('label')->get(),
|
||||||
|
'stockReasonsOut' => StockMovementReason::query()->where('direction', 'out')->orderBy('pos')->orderBy('label')->get(),
|
||||||
'storageRooms' => StorageRoom::query()->orderBy('pos')->orderBy('label')->get(),
|
'storageRooms' => StorageRoom::query()->orderBy('pos')->orderBy('label')->get(),
|
||||||
'storageShelves' => StorageShelf::query()->orderBy('pos')->orderBy('label')->get(),
|
'storageShelves' => StorageShelf::query()->orderBy('pos')->orderBy('label')->get(),
|
||||||
'storageSlots' => StorageSlot::query()->orderBy('pos')->orderBy('label')->get(),
|
'storageSlots' => StorageSlot::query()->orderBy('pos')->orderBy('label')->get(),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Inventory\StoreProductStockMovementRequest;
|
use App\Http\Requests\Inventory\StoreProductStockMovementRequest;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\ProductStockMovement;
|
use App\Models\ProductStockMovement;
|
||||||
|
use App\Models\StockMovementReason;
|
||||||
use App\Services\ProductStockService;
|
use App\Services\ProductStockService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
@ -59,10 +60,23 @@ class ProductStockController extends Controller
|
||||||
|
|
||||||
return view('admin.inventory.product-stock.index', [
|
return view('admin.inventory.product-stock.index', [
|
||||||
'rows' => $rows,
|
'rows' => $rows,
|
||||||
'reasons' => $this->manualReasons(),
|
// AP-48: getrennte, pflegbare Buchungsgründe je Richtung.
|
||||||
|
'reasonsIn' => $this->reasonsFor('in'),
|
||||||
|
'reasonsOut' => $this->reasonsFor('out'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
protected function reasonsFor(string $direction): array
|
||||||
|
{
|
||||||
|
return StockMovementReason::query()->active()
|
||||||
|
->where('direction', $direction)
|
||||||
|
->orderBy('pos')->orderBy('label')
|
||||||
|
->pluck('label')->all();
|
||||||
|
}
|
||||||
|
|
||||||
public function storeMovement(StoreProductStockMovementRequest $request, Product $product): RedirectResponse
|
public function storeMovement(StoreProductStockMovementRequest $request, Product $product): RedirectResponse
|
||||||
{
|
{
|
||||||
$data = $request->validated();
|
$data = $request->validated();
|
||||||
|
|
@ -127,22 +141,6 @@ class ProductStockController extends Controller
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<int, string>
|
|
||||||
*/
|
|
||||||
protected function manualReasons(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
__('Initialbestand'),
|
|
||||||
__('Korrektur'),
|
|
||||||
__('Inventur'),
|
|
||||||
__('Retoure'),
|
|
||||||
__('Testervergabe'),
|
|
||||||
__('Verlust / Bruch'),
|
|
||||||
__('Sonstiges'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<int, int>
|
* @return array<int, int>
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin\Inventory;
|
||||||
|
|
||||||
|
use App\Http\Requests\Inventory\StoreStockMovementReasonRequest;
|
||||||
|
use App\Models\StockMovementReason;
|
||||||
|
|
||||||
|
class StockMovementReasonController extends SettingCrudController
|
||||||
|
{
|
||||||
|
protected function modelClass(): string
|
||||||
|
{
|
||||||
|
return StockMovementReason::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function viewPrefix(): string
|
||||||
|
{
|
||||||
|
return 'admin.inventory.stock-movement-reasons';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function redirectRoute(): string
|
||||||
|
{
|
||||||
|
return 'admin.inventory.general';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function storeRequestClass(): string
|
||||||
|
{
|
||||||
|
return StoreStockMovementReasonRequest::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Neu-Formular mit vorbelegter Richtung (aus ?direction=in|out).
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
protected function newModelAttributes(): array
|
||||||
|
{
|
||||||
|
$direction = in_array(request()->query('direction'), ['in', 'out'], true)
|
||||||
|
? request()->query('direction')
|
||||||
|
: 'in';
|
||||||
|
|
||||||
|
return ['active' => true, 'pos' => 0, 'direction' => $direction];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
protected function indexOrder(): array
|
||||||
|
{
|
||||||
|
return ['direction', 'pos', 'label'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Inventory;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class StoreStockMovementReasonRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array<int, mixed>>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'label' => ['required', 'string', 'max:100'],
|
||||||
|
'direction' => ['required', Rule::in(['in', 'out'])],
|
||||||
|
'active' => ['sometimes', 'boolean'],
|
||||||
|
'pos' => ['nullable', 'integer', 'min:0', 'max:255'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
$this->merge([
|
||||||
|
'active' => $this->boolean('active'),
|
||||||
|
'pos' => $this->input('pos', '') === '' ? 0 : $this->input('pos'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Models/StockMovementReason.php
Normal file
42
app/Models/StockMovementReason.php
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AP-48: Pflegbarer Buchungsgrund für den Produktbestand, getrennt nach Richtung
|
||||||
|
* (in = Eingang, out = Ausgang).
|
||||||
|
*/
|
||||||
|
class StockMovementReason extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'label',
|
||||||
|
'direction',
|
||||||
|
'active',
|
||||||
|
'pos',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'active' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Builder<StockMovementReason> $query
|
||||||
|
* @return Builder<StockMovementReason>
|
||||||
|
*/
|
||||||
|
public function scopeActive(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where('active', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ use App\Models\ProductionIngredient;
|
||||||
use App\Models\ProductionPackaging;
|
use App\Models\ProductionPackaging;
|
||||||
use App\Models\StockDisposal;
|
use App\Models\StockDisposal;
|
||||||
use App\Models\StockEntry;
|
use App\Models\StockEntry;
|
||||||
|
use App\Models\StockMovementReason;
|
||||||
use App\Models\StorageSegment;
|
use App\Models\StorageSegment;
|
||||||
use App\Models\Supplier;
|
use App\Models\Supplier;
|
||||||
use App\Models\SupplierCategory;
|
use App\Models\SupplierCategory;
|
||||||
|
|
@ -50,6 +51,7 @@ class DeletionGuard
|
||||||
// Bewusst immer löschbar (keine eingehenden Referenzen):
|
// Bewusst immer löschbar (keine eingehenden Referenzen):
|
||||||
$model instanceof DeliveryTime,
|
$model instanceof DeliveryTime,
|
||||||
$model instanceof DisposalReason,
|
$model instanceof DisposalReason,
|
||||||
|
$model instanceof StockMovementReason,
|
||||||
$model instanceof StockDisposal,
|
$model instanceof StockDisposal,
|
||||||
$model instanceof Production => [],
|
$model instanceof Production => [],
|
||||||
default => [],
|
default => [],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* AP-48 (Kundenentscheid 26.08.2026): Buchungsgründe für den Produktbestand
|
||||||
|
* werden pflegbar — mit GETRENNTEN Listen für Eingang und Ausgang. Bisher waren
|
||||||
|
* die Gründe fest im Controller hinterlegt (eine Liste für beide Richtungen).
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('stock_movement_reasons', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('label', 100);
|
||||||
|
$table->enum('direction', ['in', 'out']);
|
||||||
|
$table->unsignedTinyInteger('pos')->default(0);
|
||||||
|
$table->boolean('active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
});
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
$seed = [
|
||||||
|
'in' => ['Initialbestand', 'Korrektur', 'Inventur', 'Retoure', 'Sonstiges'],
|
||||||
|
'out' => ['Korrektur', 'Inventur', 'Testervergabe', 'Verlust / Bruch', 'Sonstiges'],
|
||||||
|
];
|
||||||
|
$rows = [];
|
||||||
|
foreach ($seed as $direction => $labels) {
|
||||||
|
foreach ($labels as $pos => $label) {
|
||||||
|
$rows[] = [
|
||||||
|
'label' => $label,
|
||||||
|
'direction' => $direction,
|
||||||
|
'pos' => $pos,
|
||||||
|
'active' => true,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB::table('stock_movement_reasons')->insert($rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('stock_movement_reasons');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -173,6 +173,59 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- AP-48: Buchungsgründe Produktbestand — getrennte Listen für Eingang und Ausgang --}}
|
||||||
|
@foreach ([['in', __('Eingangs-Gründe (Produktbestand)'), $stockReasonsIn], ['out', __('Ausgangs-Gründe (Produktbestand)'), $stockReasonsOut]] as [$dir, $title, $reasons])
|
||||||
|
<div class="wawi-card">
|
||||||
|
<div class="wawi-card__header">
|
||||||
|
<span>{{ $title }}</span>
|
||||||
|
<a href="{{ route('admin.inventory.stock-movement-reasons.create', ['direction' => $dir]) }}" class="btn btn-sm btn-primary">{{ __('Neu anlegen') }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table wawi-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="max-width: 60px;"> </th>
|
||||||
|
<th>{{ __('Bezeichnung') }}</th>
|
||||||
|
<th>{{ __('Status') }}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($reasons as $reason)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{{ route('admin.inventory.stock-movement-reasons.edit', $reason) }}" class="btn icon-btn btn-sm btn-primary">
|
||||||
|
<span class="far fa-edit"></span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ $reason->label }}</td>
|
||||||
|
<td>
|
||||||
|
@if ($reason->active)
|
||||||
|
<span class="wawi-pill wawi-pill--ok">{{ __('Aktiv') }}</span>
|
||||||
|
@else
|
||||||
|
<span class="wawi-pill wawi-pill--danger">{{ __('Inaktiv') }}</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form action="{{ route('admin.inventory.stock-movement-reasons.destroy', $reason) }}" method="post" class="d-inline"
|
||||||
|
onsubmit="return confirm('{{ __('Really delete entry?') }}');">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-link text-danger p-0" title="{{ __('Delete') }}"><i class="far fa-trash-alt"></i></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="4"><div class="wawi-empty">{{ __('Noch keine Gründe angelegt.') }}</div></td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
@include('admin.inventory.partials.setting-list-card', [
|
@include('admin.inventory.partials.setting-list-card', [
|
||||||
'title' => __('Lagerorte: Räume'),
|
'title' => __('Lagerorte: Räume'),
|
||||||
'routePrefix' => 'admin.inventory.storage-rooms',
|
'routePrefix' => 'admin.inventory.storage-rooms',
|
||||||
|
|
|
||||||
|
|
@ -176,11 +176,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="ps-move-reason">{{ __('Grund') }} <span class="text-danger">*</span></label>
|
<label for="ps-move-reason">{{ __('Grund') }} <span class="text-danger">*</span></label>
|
||||||
<select name="reason" id="ps-move-reason" class="form-control" required>
|
{{-- AP-48: Optionen werden je Richtung (Eingang/Ausgang) per JS gefüllt --}}
|
||||||
@foreach ($reasons as $reason)
|
<select name="reason" id="ps-move-reason" class="form-control" required></select>
|
||||||
<option value="{{ $reason }}">{{ $reason }}</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group mb-0">
|
<div class="form-group mb-0">
|
||||||
<label for="ps-move-note">{{ __('Hinweis') }}</label>
|
<label for="ps-move-note">{{ __('Hinweis') }}</label>
|
||||||
|
|
@ -247,6 +244,14 @@
|
||||||
|
|
||||||
var moveTemplate = "{{ route('admin.inventory.product-stock.movement', ['product' => '__ID__']) }}";
|
var moveTemplate = "{{ route('admin.inventory.product-stock.movement', ['product' => '__ID__']) }}";
|
||||||
var moveStock = 0;
|
var moveStock = 0;
|
||||||
|
// AP-48: getrennte, pflegbare Buchungsgründe je Richtung.
|
||||||
|
var psReasons = {in: @json($reasonsIn), out: @json($reasonsOut)};
|
||||||
|
function psPopulateReasons(direction) {
|
||||||
|
var $sel = $('#ps-move-reason').empty();
|
||||||
|
(psReasons[direction] || []).forEach(function (r) {
|
||||||
|
$sel.append($('<option></option>').val(r).text(r));
|
||||||
|
});
|
||||||
|
}
|
||||||
$('.js-ps-move').on('click', function () {
|
$('.js-ps-move').on('click', function () {
|
||||||
var id = $(this).data('product');
|
var id = $(this).data('product');
|
||||||
var name = $(this).data('name');
|
var name = $(this).data('name');
|
||||||
|
|
@ -254,6 +259,7 @@
|
||||||
moveStock = parseInt($(this).data('stock'), 10) || 0;
|
moveStock = parseInt($(this).data('stock'), 10) || 0;
|
||||||
$('#ps-move-form').attr('action', moveTemplate.replace('__ID__', id));
|
$('#ps-move-form').attr('action', moveTemplate.replace('__ID__', id));
|
||||||
$('#ps-move-direction').val(direction);
|
$('#ps-move-direction').val(direction);
|
||||||
|
psPopulateReasons(direction);
|
||||||
$('#ps-move-product').text(name);
|
$('#ps-move-product').text(name);
|
||||||
$('#ps-move-stock').text(moveStock);
|
$('#ps-move-stock').text(moveStock);
|
||||||
$('#ps-move-title').text(direction === 'in' ? '{{ __('Eingang buchen') }}' : '{{ __('Ausgang buchen') }}');
|
$('#ps-move-title').text(direction === 'in' ? '{{ __('Eingang buchen') }}' : '{{ __('Ausgang buchen') }}');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
@extends('layouts.layout-2')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="card">
|
||||||
|
<h6 class="card-header">{{ $model->exists ? __('Buchungsgrund bearbeiten') : __('Buchungsgrund anlegen') }}</h6>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="{{ $model->exists ? route('admin.inventory.stock-movement-reasons.update', $model) : route('admin.inventory.stock-movement-reasons.store') }}">
|
||||||
|
@csrf
|
||||||
|
@if($model->exists)
|
||||||
|
@method('PUT')
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="direction">{{ __('Richtung') }}</label>
|
||||||
|
<select name="direction" id="direction" class="form-control @error('direction') is-invalid @enderror">
|
||||||
|
<option value="in" @selected(old('direction', $model->direction) === 'in')>{{ __('Eingang') }}</option>
|
||||||
|
<option value="out" @selected(old('direction', $model->direction) === 'out')>{{ __('Ausgang') }}</option>
|
||||||
|
</select>
|
||||||
|
@error('direction')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="label">{{ __('Bezeichnung') }}</label>
|
||||||
|
<input type="text" name="label" id="label" maxlength="100"
|
||||||
|
class="form-control @error('label') is-invalid @enderror"
|
||||||
|
value="{{ old('label', $model->label) }}" placeholder="{{ __('z. B. Retoure') }}" required>
|
||||||
|
@error('label')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pos">{{ __('Sortierung') }}</label>
|
||||||
|
<input type="number" name="pos" id="pos" min="0" max="255"
|
||||||
|
class="form-control @error('pos') is-invalid @enderror"
|
||||||
|
value="{{ old('pos', $model->pos) }}">
|
||||||
|
@error('pos')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" name="active" value="1" class="custom-control-input"
|
||||||
|
@checked(old('active', $model->active))>
|
||||||
|
<span class="custom-control-label">{{ __('Aktiv') }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">{{ __('Speichern') }}</button>
|
||||||
|
<a href="{{ route('admin.inventory.general') }}" class="btn btn-outline-secondary">{{ __('Zurück') }}</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -16,6 +16,7 @@ use App\Http\Controllers\Admin\Inventory\ProductStockController as InventoryProd
|
||||||
use App\Http\Controllers\Admin\Inventory\RawMaterialStockController as InventoryRawMaterialStockController;
|
use App\Http\Controllers\Admin\Inventory\RawMaterialStockController as InventoryRawMaterialStockController;
|
||||||
use App\Http\Controllers\Admin\Inventory\StockDisposalController as InventoryStockDisposalController;
|
use App\Http\Controllers\Admin\Inventory\StockDisposalController as InventoryStockDisposalController;
|
||||||
use App\Http\Controllers\Admin\Inventory\StockEntryController as InventoryStockEntryController;
|
use App\Http\Controllers\Admin\Inventory\StockEntryController as InventoryStockEntryController;
|
||||||
|
use App\Http\Controllers\Admin\Inventory\StockMovementReasonController;
|
||||||
use App\Http\Controllers\Admin\Inventory\StorageRoomController as InventoryStorageRoomController;
|
use App\Http\Controllers\Admin\Inventory\StorageRoomController as InventoryStorageRoomController;
|
||||||
use App\Http\Controllers\Admin\Inventory\StorageShelfController as InventoryStorageShelfController;
|
use App\Http\Controllers\Admin\Inventory\StorageShelfController as InventoryStorageShelfController;
|
||||||
use App\Http\Controllers\Admin\Inventory\StorageSlotController as InventoryStorageSlotController;
|
use App\Http\Controllers\Admin\Inventory\StorageSlotController as InventoryStorageSlotController;
|
||||||
|
|
@ -294,6 +295,7 @@ Route::domain(config('app.domain'))->group(function () {
|
||||||
Route::resource('tax-rates', InventoryTaxRateController::class)->except(['index', 'show']);
|
Route::resource('tax-rates', InventoryTaxRateController::class)->except(['index', 'show']);
|
||||||
Route::resource('delivery-times', InventoryDeliveryTimeController::class)->except(['index', 'show']);
|
Route::resource('delivery-times', InventoryDeliveryTimeController::class)->except(['index', 'show']);
|
||||||
Route::resource('disposal-reasons', InventoryDisposalReasonController::class)->except(['index', 'show']);
|
Route::resource('disposal-reasons', InventoryDisposalReasonController::class)->except(['index', 'show']);
|
||||||
|
Route::resource('stock-movement-reasons', StockMovementReasonController::class)->except(['index', 'show']);
|
||||||
Route::resource('storage-rooms', InventoryStorageRoomController::class)->except(['index', 'show']);
|
Route::resource('storage-rooms', InventoryStorageRoomController::class)->except(['index', 'show']);
|
||||||
Route::resource('storage-shelves', InventoryStorageShelfController::class)->except(['index', 'show']);
|
Route::resource('storage-shelves', InventoryStorageShelfController::class)->except(['index', 'show']);
|
||||||
Route::resource('storage-slots', InventoryStorageSlotController::class)->except(['index', 'show']);
|
Route::resource('storage-slots', InventoryStorageSlotController::class)->except(['index', 'show']);
|
||||||
|
|
|
||||||
57
tests/Feature/StockMovementReasonTest.php
Normal file
57
tests/Feature/StockMovementReasonTest.php
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\StockMovementReason;
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class, RefreshDatabase::class);
|
||||||
|
|
||||||
|
function smrAdmin(): User
|
||||||
|
{
|
||||||
|
$user = User::query()->create(['email' => uniqid('smr_', true).'@test.example', 'password' => bcrypt('password')]);
|
||||||
|
$user->forceFill(['admin' => 9, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false])->save();
|
||||||
|
|
||||||
|
return $user->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
test('AP-48: seed liefert getrennte gründe für eingang und ausgang', function () {
|
||||||
|
expect(StockMovementReason::query()->where('direction', 'in')->count())->toBeGreaterThan(0)
|
||||||
|
->and(StockMovementReason::query()->where('direction', 'out')->count())->toBeGreaterThan(0)
|
||||||
|
// „Testervergabe" ist ein Ausgangs-, kein Eingangsgrund
|
||||||
|
->and(StockMovementReason::query()->where('direction', 'out')->where('label', 'Testervergabe')->exists())->toBeTrue()
|
||||||
|
->and(StockMovementReason::query()->where('direction', 'in')->where('label', 'Testervergabe')->exists())->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AP-48: grund kann mit richtung angelegt werden', function () {
|
||||||
|
$this->actingAs(smrAdmin(), 'user');
|
||||||
|
|
||||||
|
$this->post(route('admin.inventory.stock-movement-reasons.store'), [
|
||||||
|
'label' => 'AP48-Sonderentnahme',
|
||||||
|
'direction' => 'out',
|
||||||
|
'active' => '1',
|
||||||
|
'pos' => '3',
|
||||||
|
])->assertRedirect(route('admin.inventory.general'));
|
||||||
|
|
||||||
|
$reason = StockMovementReason::query()->where('label', 'AP48-Sonderentnahme')->firstOrFail();
|
||||||
|
expect($reason->direction)->toBe('out')->and($reason->active)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AP-48: produktbestand-maske liefert die gründe je richtung', function () {
|
||||||
|
$this->actingAs(smrAdmin(), 'user');
|
||||||
|
StockMovementReason::query()->create(['label' => 'AP48-NurEingang', 'direction' => 'in', 'active' => true, 'pos' => 9]);
|
||||||
|
|
||||||
|
$this->get(route('admin.inventory.product-stock.index'))
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee('AP48-NurEingang');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AP-48: grund ist immer löschbar', function () {
|
||||||
|
$this->actingAs(smrAdmin(), 'user');
|
||||||
|
$reason = StockMovementReason::query()->create(['label' => 'AP48-Weg', 'direction' => 'in', 'active' => true, 'pos' => 9]);
|
||||||
|
|
||||||
|
$this->delete(route('admin.inventory.stock-movement-reasons.destroy', $reason))
|
||||||
|
->assertRedirect(route('admin.inventory.general'));
|
||||||
|
|
||||||
|
expect(StockMovementReason::query()->find($reason->id))->toBeNull();
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue