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).
266 lines
8.4 KiB
PHP
266 lines
8.4 KiB
PHP
<?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'),
|
||
],
|
||
];
|
||
}
|
||
}
|