Warenwirtschaft: Anforderungsrunde 12.06. — Plan V5.0 + AP-26/AP-25/AP-22
Neue Anforderungen (docs/) interpretiert und als Entwicklungsplan V5.0 (AP-20 bis AP-28) aufgenommen; erste drei Pakete umgesetzt: AP-26 Ausschuss-Gründe konfigurierbar: - Stammdaten-Tabelle disposal_reasons + CRUD unter Einstellungen → Allgemein - StockDisposalController liest aktive DB-Gründe statt hartkodierter Liste - Seeder übernimmt die bisherigen 6 Gründe idempotent AP-25 Lieferbestand — Datum statt Tage: - "Nicht vorrätig" wird über Datepicker "Wieder lieferbar ab" gepflegt; Resttage-Hinweis zählt täglich automatisch herunter - Interne Bestellliste wieder kaufbar: Hinweis erscheint zusätzlich zu den Mengen-Buttons (VP entscheidet selbst) AP-22 Produktbestand-Erweiterungen: - Default-Sortierung nach Dringlichkeit, Status-Kopf toggelt - Alle vier Status-Kacheln als Filter klickbar - Neue Spalte "Verbrauch/Monat" (Ø Abgänge der letzten 6 Monate) - Produkt-Flag "Im Produktbestand anzeigen" (products.show_in_product_stock) Tests: 77 grün (DisposalReasonSettings 8, ProductOutOfStock 8, ProductStock 13 + Regression). Hinweise-Doku + Plan-Protokoll fortgeschrieben; nächster Schritt laut Plan: AP-21 (INCI-Erweiterungen). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a8f6fef38e
commit
e53201f229
32 changed files with 1377 additions and 94 deletions
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Inventory;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Inventory\StoreDisposalReasonRequest;
|
||||
use App\Http\Requests\Inventory\UpdateDisposalReasonRequest;
|
||||
use App\Models\DisposalReason;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class DisposalReasonController extends Controller
|
||||
{
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.inventory.disposal-reasons.form', [
|
||||
'model' => new DisposalReason(['active' => true, 'pos' => 0]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreDisposalReasonRequest $request): RedirectResponse
|
||||
{
|
||||
DisposalReason::create($request->validated());
|
||||
|
||||
\Session::flash('alert-save', '1');
|
||||
|
||||
return redirect()->route('admin.inventory.general');
|
||||
}
|
||||
|
||||
public function edit(DisposalReason $disposalReason): View
|
||||
{
|
||||
return view('admin.inventory.disposal-reasons.form', [
|
||||
'model' => $disposalReason,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateDisposalReasonRequest $request, DisposalReason $disposalReason): RedirectResponse
|
||||
{
|
||||
$disposalReason->update($request->validated());
|
||||
|
||||
\Session::flash('alert-save', '1');
|
||||
|
||||
return redirect()->route('admin.inventory.general');
|
||||
}
|
||||
|
||||
public function destroy(DisposalReason $disposalReason): RedirectResponse
|
||||
{
|
||||
$disposalReason->delete();
|
||||
|
||||
\Session::flash('alert-success', __('Eintrag gelöscht'));
|
||||
|
||||
return redirect()->route('admin.inventory.general');
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin\Inventory;
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DeliveryTime;
|
||||
use App\Models\DisposalReason;
|
||||
use App\Models\TaxRate;
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
||||
|
|
@ -14,6 +15,7 @@ class GeneralSettingController extends Controller
|
|||
return view('admin.inventory.general.index', [
|
||||
'taxRates' => TaxRate::query()->orderBy('pos')->orderBy('percent')->get(),
|
||||
'deliveryTimes' => DeliveryTime::query()->orderBy('pos')->orderBy('label')->get(),
|
||||
'disposalReasons' => DisposalReason::query()->orderBy('pos')->orderBy('label')->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,27 +22,40 @@ class ProductStockController extends Controller
|
|||
$products = Product::query()
|
||||
->where('active', true)
|
||||
->where('is_set', false)
|
||||
->where('show_in_product_stock', true)
|
||||
->whereNull('main_product_id')
|
||||
->with('images')
|
||||
->orderBy('pos')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$stock = $this->productStockService->currentStockByProduct($products->pluck('id')->all());
|
||||
$ids = $products->pluck('id')->all();
|
||||
$stock = $this->productStockService->currentStockByProduct($ids);
|
||||
$consumption = $this->productStockService->monthlyConsumptionByProduct($ids);
|
||||
|
||||
$rows = $products->map(function (Product $product) use ($stock) {
|
||||
$statusRank = ['critical' => 0, 'warning' => 1, 'ok' => 2];
|
||||
|
||||
$rows = $products->map(function (Product $product) use ($stock, $consumption) {
|
||||
$current = $stock[$product->id] ?? 0;
|
||||
|
||||
return [
|
||||
'product' => $product,
|
||||
'stock' => $current,
|
||||
'monthly_consumption' => $consumption[$product->id] ?? 0.0,
|
||||
'status' => $this->productStockService->productStatus(
|
||||
$current,
|
||||
$product->min_product_stock,
|
||||
$product->critical_product_stock,
|
||||
),
|
||||
];
|
||||
});
|
||||
})
|
||||
// AP-22: Default-Sortierung nach Dringlichkeit (kritisch → niedrig → ok), innerhalb nach Bestand/Name.
|
||||
->sortBy([
|
||||
fn (array $a, array $b) => ($statusRank[$a['status']] <=> $statusRank[$b['status']])
|
||||
?: ($a['stock'] <=> $b['stock'])
|
||||
?: strcasecmp($a['product']->name, $b['product']->name),
|
||||
])
|
||||
->values();
|
||||
|
||||
return view('admin.inventory.product-stock.index', [
|
||||
'rows' => $rows,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin\Inventory;
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Inventory\StoreStockDisposalRequest;
|
||||
use App\Models\DisposalReason;
|
||||
use App\Models\Ingredient;
|
||||
use App\Models\Location;
|
||||
use App\Models\StockDisposal;
|
||||
|
|
@ -109,17 +110,17 @@ class StockDisposalController extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* Aktive Ausschuss-Gründe aus den Einstellungen (Warenwirtschaft → Allgemein).
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected function reasons(): array
|
||||
{
|
||||
return [
|
||||
__('Bruch / Beschädigung'),
|
||||
__('Verfall / MHD überschritten'),
|
||||
__('Qualitätsmangel'),
|
||||
__('Schwund / Inventurdifferenz'),
|
||||
__('Muster / Testverbrauch'),
|
||||
__('Sonstiges'),
|
||||
];
|
||||
return DisposalReason::query()
|
||||
->active()
|
||||
->orderBy('pos')
|
||||
->orderBy('label')
|
||||
->pluck('label')
|
||||
->all();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -319,21 +319,23 @@ class OrderController extends Controller
|
|||
$qty = isset($cartItem->qty) ? $cartItem->qty : 0;
|
||||
$rowId = isset($cartItem->rowId) ? $cartItem->rowId : '';
|
||||
|
||||
// AP-25: „Nicht vorrätig" ist nur ein Hinweis — der VP entscheidet selbst,
|
||||
// ob er die Ware später bekommt. Die Mengen-Buttons bleiben immer aktiv.
|
||||
$controls = '';
|
||||
if ($product->isOutOfStock()) {
|
||||
$controls = '<div class="product-stock-hint">'.e($product->outOfStockNotice()).'</div>';
|
||||
} else {
|
||||
$controls = '<div class="no-line-break input-group-min-w">
|
||||
<div class="input-group d-inline-flex w-auto">
|
||||
<span class="input-group-prepend">
|
||||
<button type="button" class="btn btn-secondary icon-btn md-btn-extra remove-product-basket" data-row-id="'.$rowId.'" data-product-id="'.$product->id.'">-</button>
|
||||
</span>
|
||||
<input type="text" class="form-control text-center input-extra table-input-event-onchange" name="product_qty_'.$product->id.'" data-row-id="'.$rowId.'" data-product-id="'.$product->id.'" value="'.$qty.'">
|
||||
<span class="input-group-append">
|
||||
<button type="button" class="btn btn-secondary icon-btn md-btn-extra add-product-basket" data-row-id="'.$rowId.'" data-product-id="'.$product->id.'">+</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>';
|
||||
$controls .= '<div class="product-stock-hint">'.e($product->outOfStockNotice()).'</div>';
|
||||
}
|
||||
$controls .= '<div class="no-line-break input-group-min-w">
|
||||
<div class="input-group d-inline-flex w-auto">
|
||||
<span class="input-group-prepend">
|
||||
<button type="button" class="btn btn-secondary icon-btn md-btn-extra remove-product-basket" data-row-id="'.$rowId.'" data-product-id="'.$product->id.'">-</button>
|
||||
</span>
|
||||
<input type="text" class="form-control text-center input-extra table-input-event-onchange" name="product_qty_'.$product->id.'" data-row-id="'.$rowId.'" data-product-id="'.$product->id.'" value="'.$qty.'">
|
||||
<span class="input-group-append">
|
||||
<button type="button" class="btn btn-secondary icon-btn md-btn-extra add-product-basket" data-row-id="'.$rowId.'" data-product-id="'.$product->id.'">+</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>';
|
||||
|
||||
return '<strong>'.$product->name.'</strong><br>'.$controls;
|
||||
})
|
||||
|
|
|
|||
33
app/Http/Requests/Inventory/StoreDisposalReasonRequest.php
Normal file
33
app/Http/Requests/Inventory/StoreDisposalReasonRequest.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\Inventory;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreDisposalReasonRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, string>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'label' => ['required', 'string', 'max:100'],
|
||||
'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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\Inventory;
|
||||
|
||||
class UpdateDisposalReasonRequest extends StoreDisposalReasonRequest {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue