gruene-seele/app/Models/PackagingItem.php
Kevin Adametz 30d4e28696 Warenwirtschaft: AP-51 — Lagerplatz-Felder für Produkte & Verpackung
Lagerplatz-Zuordnung (Raum/Regal/Fach) gab es bisher nur bei Rohstoffen. Jetzt
dieselbe Storage-Hierarchie (StorageRoom/Shelf/Slot) auch für Produkte und
Verpackungsartikel, inkl. Anzeige an den passenden Stellen.

- Migration: storage_room_id/shelf/slot an products + packaging_items
  (nullable, FK constrained, nullOnDelete).
- Models Product + PackagingItem: fillable + storageRoom/Shelf/Slot-Relationen +
  storageLabel() („Raum 1 | Regal 3 | A").
- Produktformular (Inventar-Abschnitt) + Verpackungsformular: Auswahl-Zeile
  Raum/Regal/Fach; Optionen kommen aktiv/sortiert aus den zentral pflegbaren
  Bausteinen (Einstellungen → Allgemein).
- Speichern: ProductRepository normalisiert leere Auswahl -> null (FK);
  PackagingItem-Requests + Repository um die drei Felder erweitert.
- Anzeige: Produktbestand (Sub-Zeile mit Marker), Produktverpackung-Übersicht
  (neue Spalte „Lagerplatz").
- DeletionGuard: Lagerbaustein ist nicht löschbar, solange Produkte/Verpackung
  darauf verweisen (analog Rohstoffe).

Tests: 4 neue (Produkt speichern + leer->null, Verpackung speichern + Anzeige,
Löschsperre, freier Baustein löschbar). Migration auf Dev-/Live-DB gelaufen.
Suite 331 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 09:46:40 +00:00

118 lines
2.8 KiB
PHP

<?php
namespace App\Models;
use Database\Factories\PackagingItemFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class PackagingItem extends Model
{
/** @use HasFactory<PackagingItemFactory> */
use HasFactory, SoftDeletes;
protected $fillable = [
'packaging_material_id',
'supplier_id',
'name',
'category',
'weight_grams',
'min_stock_alert',
'url',
'product_id',
'storage_room_id',
'storage_shelf_id',
'storage_slot_id',
'active',
];
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'active' => 'boolean',
'weight_grams' => 'decimal:2',
];
}
/**
* @return BelongsTo<PackagingMaterial, $this>
*/
public function packagingMaterial(): BelongsTo
{
return $this->belongsTo(PackagingMaterial::class);
}
/**
* @return BelongsTo<Supplier, $this>
*/
public function supplier(): BelongsTo
{
return $this->belongsTo(Supplier::class);
}
/**
* @return BelongsTo<Product, $this>
*/
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
/**
* Produkte, die diesen Verpackungsartikel in der Stückliste führen (BOM).
*
* @return BelongsToMany<Product, $this>
*/
public function products(): BelongsToMany
{
return $this->belongsToMany(Product::class, 'product_packagings')
->withPivot('quantity', 'pos')
->withTimestamps();
}
/**
* AP-51: Lagerplatz (Raum/Regal/Fach) — gleiche Hierarchie wie beim Rohstoff.
*
* @return BelongsTo<StorageRoom, $this>
*/
public function storageRoom(): BelongsTo
{
return $this->belongsTo(StorageRoom::class);
}
/**
* @return BelongsTo<StorageShelf, $this>
*/
public function storageShelf(): BelongsTo
{
return $this->belongsTo(StorageShelf::class);
}
/**
* @return BelongsTo<StorageSlot, $this>
*/
public function storageSlot(): BelongsTo
{
return $this->belongsTo(StorageSlot::class);
}
/**
* Lagerplatz-Anzeige im Schema „Raum 1 | Regal 3 | A" (nur gepflegte Teile).
*/
public function storageLabel(): ?string
{
$parts = array_filter([
$this->storageRoom?->label,
$this->storageShelf?->label,
$this->storageSlot?->label,
]);
return $parts === [] ? null : implode(' | ', $parts);
}
}