gruene-seele/app/Http/Controllers/Admin/Inventory/StockEntryController.php
Kevin Adametz 78679e0c55 Warenwirtschaft: AP-00 bis AP-08 + aktualisierter Entwicklungsplan
Umsetzung der Warenwirtschafts-/Produktmanagement-Erweiterung gemaess
Entwicklungsplan V4.0:

- AP-00: Regressionsbasis fuer 5.1-Features (ProductPhase51Test)
- AP-01: URL-Bugfixes B1/B2 (suppliers/packaging-items, breitere url-Spalten)
- AP-04/04.1: iPad-taugliche, vereinheitlichte Tabellen-Aktionen
- AP-05: Einstellungen "Allgemein" mit UST-Saetzen (tax_rates) und
  Lieferzeit-Vorlagen (delivery_times, inkl. Tage-Feld)
- AP-06: Lieferanten um Bestellweg, Bestell-Mail/-URL und Lieferzeit erweitert
- AP-07/07.1: INCI um Lieferanten-Mehrfachwahl, UST und Lieferzeit erweitert;
  Lieferanten-Detailansicht im Modal mit pflegbaren INCI-/Verpackungslisten
- AP-08: Einkauf um UST-Snapshot, Netto/Brutto-Automatik und Duplizieren erweitert

Entwicklungsplan aktualisiert: alle Klaerungspunkte (§5) vom Kunden beantwortet
und in die jeweiligen APs eingearbeitet (AP-02/03/09/13/15), neues AP-18
(Hinweise-Doku unter Einstellungen) ergaenzt. Naechster Schritt eindeutig
markiert: AP-09 (Produktion auf Hersteller-Rezeptur, kein Fallback, Warnung).
2026-06-02 16:30:42 +00:00

266 lines
8.4 KiB
PHP
Raw 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
namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Http\Requests\Inventory\ReceiveStockEntryRequest;
use App\Http\Requests\Inventory\StoreStockEntryRequest;
use App\Http\Requests\Inventory\UpdateStockEntryRequest;
use App\Models\Ingredient;
use App\Models\Location;
use App\Models\MaterialQuality;
use App\Models\PackagingItem;
use App\Models\StockEntry;
use App\Models\Supplier;
use App\Models\TaxRate;
use App\Repositories\StockEntryRepository;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class StockEntryController extends Controller
{
public function __construct(
protected StockEntryRepository $stockEntryRepository
) {}
public function index(): View
{
return view('admin.inventory.stock-entries.index', array_merge($this->formSharedData(), [
'values' => $this->stockEntryRepository->listForIndex(),
]));
}
public function create(): View|RedirectResponse
{
if (! auth()->user()->isAdmin()) {
return redirect()->route('home');
}
return view('admin.inventory.stock-entries.create', array_merge($this->formSharedData(), [
'model' => new StockEntry([
'ordered_at' => now()->toDateString(),
'entry_type' => 'ingredient',
]),
]));
}
public function store(StoreStockEntryRequest $request): RedirectResponse
{
if (! $request->user()->isAdmin()) {
return redirect()->route('home');
}
$data = $request->validatedPayload();
$data['ordered_by'] = (int) auth()->id();
$this->stockEntryRepository->create($data);
\Session::flash('alert-save', '1');
return redirect()->route('admin.inventory.stock-entries.index');
}
public function show(StockEntry $stockEntry): View
{
$stockEntry->load([
'ingredient',
'packagingItem.packagingMaterial',
'supplier',
'location',
'quality',
'orderedByUser.account',
'receivedByUser.account',
]);
return view('admin.inventory.stock-entries.show', array_merge($this->formSharedData(), [
'model' => $stockEntry,
'materialQualities' => MaterialQuality::query()->orderBy('pos')->orderBy('name')->get(),
]));
}
public function edit(StockEntry $stockEntry): View|RedirectResponse
{
if (! auth()->user()->isAdmin()) {
return redirect()->route('home');
}
if (! $stockEntry->isPending()) {
\Session::flash('alert-error', __('Nur offene Bestellungen können bearbeitet werden.'));
return redirect()->route('admin.inventory.stock-entries.show', $stockEntry);
}
$stockEntry->load(['ingredient', 'packagingItem']);
return view('admin.inventory.stock-entries.edit', array_merge($this->formSharedData(), [
'model' => $stockEntry,
]));
}
public function update(UpdateStockEntryRequest $request, StockEntry $stockEntry): RedirectResponse
{
if (! $request->user()->isAdmin()) {
return redirect()->route('home');
}
if (! $stockEntry->isPending()) {
\Session::flash('alert-error', __('Nur offene Bestellungen können bearbeitet werden.'));
return redirect()->route('admin.inventory.stock-entries.show', $stockEntry);
}
$this->stockEntryRepository->update($stockEntry, $request->validatedPayload());
\Session::flash('alert-save', '1');
return redirect()->route('admin.inventory.stock-entries.index');
}
public function copy(StockEntry $stockEntry): RedirectResponse
{
if (! auth()->user()->isAdmin()) {
return redirect()->route('home');
}
$data = $stockEntry->only([
'entry_type',
'ingredient_id',
'packaging_item_id',
'supplier_id',
'location_id',
'ordered_quantity',
'price_per_kg',
'price_per_kg_gross',
'price_total',
'tax_rate_id',
'tax_rate_percent',
'quality_id',
]);
$data['ordered_at'] = now()->toDateString();
$data['ordered_by'] = (int) auth()->id();
$data['status'] = 'pending';
$copy = $this->stockEntryRepository->create($data);
\Session::flash('alert-warning', __('Kopie angelegt bitte Menge/Charge prüfen und speichern.'));
return redirect()->route('admin.inventory.stock-entries.edit', $copy);
}
public function destroy(StockEntry $stockEntry): RedirectResponse
{
if (! auth()->user()->isAdmin()) {
return redirect()->route('home');
}
if (! $stockEntry->isPending()) {
\Session::flash('alert-error', __('Nur offene Bestellungen können gelöscht werden.'));
return redirect()->route('admin.inventory.stock-entries.index');
}
$stockEntry->delete();
\Session::flash('alert-success', __('Eintrag gelöscht'));
return redirect()->route('admin.inventory.stock-entries.index');
}
public function receive(ReceiveStockEntryRequest $request, StockEntry $stockEntry): RedirectResponse
{
if (! $stockEntry->isPending()) {
\Session::flash('alert-error', __('Eintrag nicht mehr offen.'));
return redirect()->route('admin.inventory.stock-entries.show', $stockEntry);
}
$data = $request->validated();
if ($stockEntry->entry_type !== 'ingredient') {
$data['quality_id'] = null;
$data['best_before'] = null;
}
$this->stockEntryRepository->receive($stockEntry, $data);
\Session::flash('alert-save', '1');
return redirect()->route('admin.inventory.stock-entries.show', $stockEntry->fresh());
}
public function searchIngredients(Request $request): JsonResponse
{
$term = trim((string) $request->query('q', ''));
if (mb_strlen($term) < 1) {
return response()->json(['results' => []]);
}
$rows = Ingredient::query()
->where('active', true)
->where(function ($query) use ($term) {
$query->where('name', 'like', '%'.$term.'%')
->orWhere('inci', 'like', '%'.$term.'%');
})
->orderBy('name')
->limit(30)
->get(['id', 'name', 'inci']);
$results = $rows->map(function (Ingredient $i) {
$text = $i->name;
if ($i->inci) {
$text .= ' ('.$i->inci.')';
}
return ['id' => $i->id, 'text' => $text];
})->values()->all();
return response()->json(['results' => $results]);
}
public function searchPackagingItems(Request $request): JsonResponse
{
$term = trim((string) $request->query('q', ''));
$entryType = $request->query('entry_type');
$categoryMap = [
'packaging' => 'packaging',
'shipping' => 'shipping',
];
$query = PackagingItem::query()
->where('active', true);
if ($entryType && isset($categoryMap[$entryType])) {
$query->where('category', $categoryMap[$entryType]);
}
if (mb_strlen($term) >= 1) {
$query->where('name', 'like', '%'.$term.'%');
}
$rows = $query->orderBy('name')->limit(30)->get(['id', 'name']);
$results = $rows->map(fn (PackagingItem $p) => [
'id' => $p->id,
'text' => $p->name,
])->values()->all();
return response()->json(['results' => $results]);
}
/**
* @return array<string, mixed>
*/
protected function formSharedData(): array
{
return [
'suppliers' => Supplier::query()->where('active', true)->orderBy('name')->get(),
'locations' => Location::query()->where('active', true)->orderBy('name')->get(),
'materialQualities' => MaterialQuality::query()->orderBy('pos')->orderBy('name')->get(),
'taxRates' => TaxRate::query()->active()->orderBy('pos')->orderBy('name')->get(),
'entryTypeLabels' => [
'ingredient' => __('Rohstoff'),
'packaging' => __('Produktverpackung'),
'shipping' => __('Versandverpackung'),
],
];
}
}